Follow @SBC_Games
In past month I started exploring world of HTML5 and Javascript games. Finally I ended with Phaser game engine by Photonstorm and Typescript (in Visual Studio 2013).
During making our first games I was really missing ability to wrap bitmap text. So, I created helper class TextWraper, that is doing text wrap for me.
Why you need such a class? You can choose between bitmap and true type fonts. While true type fonts are more flexible, bitmap fonts are more visually appaeling. Unfortunately, Phaser's bitmapText cannot wrap text. If you tune your text screen with "hard coded" new lines and then you change few words or translate it into different language, you have to tune it again...
The final TextWrapper helper not only can wrap the text, but it also splits it into multiple pages returning string[], where every single element is one page of text. You just tell to it what is requested width and height of your text area.
In example below you can see the TextWrapper class in action (press buttons to change screens). All original new line characters are preserved and other that are needed to wrap long paragraphs are added. Font for the demo was created in Littera.
Using the class is as simple as this:
var text: string = 'WRAPPED BITMAP TEXT\n' + '-------------------------------------------\n\n' + ' This example demonstrates TextWrapper helper class for Phaser game engine, that allows you to easily wrap bitmap text.\n' + ' Not only it can wrap long paragraphs into lines but it also splits whole text into multiple pages - ' + 'touch buttons bellow to go to next or previous page.\n' + ' The TextWrapper preserves all original new line characters and ads others, that are necessary to wrap text correctly. ' + 'After calling wrapText() method string array is returned where each array element is single text page with all necessary new line characters.\n\n' + ' By the way, if you like background image it is from our game Shards - the brickbreaker. ' + 'It is available for Android as well as for iOS. Give it a try! Not a bad way how to create long text and make publicity to our game at once :-)'; var pages: string[] = Helper.TextWrapper.wrapText(text, 520, 260, 'Font', 25); var currentPage: number = 0; var bitmapText = this.add.bitmapText(60, 30, 'Font', pages[currentPage], 25);
As can be seen from the code, the class takes these parameters:
- text - text to wrap,
- width - width of target text area,
- height - height of target text area,
- fontName - name of the font,
- size (optional) - to calculate font scale. If omitted then scale = 1;
The TextWrapper class itself is rather long as it has to handle lot of wrapping issues. For example, it is splitting by words, but in case some word is too wide and would not fit into requested width then this word has to be split in the middle.
Whole listing for the TextWrapper class is here:
module Helper { enum eCharType { UNDEFINED = -1, SPACE = 1, NEWLINE = 2, CHARACTER = 3, //SPECIAL = 4 // for future } export class TextWrapper { static mText: string; static mTextPosition: number; static mFontData: any; // ------------------------------------------------------------------------- private static hasNext(): boolean { return TextWrapper.mTextPosition < TextWrapper.mText.length; } // ------------------------------------------------------------------------- private static getChar(): string { return TextWrapper.mText.charAt(TextWrapper.mTextPosition++); } // ------------------------------------------------------------------------- private static peekChar(): string { return TextWrapper.mText.charAt(TextWrapper.mTextPosition); } // ------------------------------------------------------------------------- private static getPosition(): number { return TextWrapper.mTextPosition; } // ------------------------------------------------------------------------- private static setPosition(aPosition: number): void { TextWrapper.mTextPosition = aPosition; } // ------------------------------------------------------------------------- private static getCharAdvance(aCharCode: number, aPrevCharCode: number): number { var charData = TextWrapper.mFontData.chars[aCharCode]; // width var advance: number = charData.xAdvance; // kerning if (aPrevCharCode > 0 && charData.kerning[aPrevCharCode]) advance += charData.kerning[aPrevCharCode]; return advance; } // ------------------------------------------------------------------------- private static getCharType(aChar: string): eCharType { if (aChar === ' ') return eCharType.SPACE; else if (/(?:\r\n|\r|\n)/.test(aChar)) return eCharType.NEWLINE; else return eCharType.CHARACTER; } // ------------------------------------------------------------------------- static wrapText(aText: string, aWidth: number, aHeight:number, aFontName: string, aSize? : number): string[] { // set vars for text processing TextWrapper.mText = aText; TextWrapper.setPosition(0); // font data TextWrapper.mFontData = PIXI.BitmapText.fonts[aFontName]; // if size not defined then take default size if (aSize === undefined) aSize = TextWrapper.mFontData.size; var scale: number = aSize / TextWrapper.mFontData.size; // height of line scaled var lineHeight: number = TextWrapper.mFontData.lineHeight * scale; // instead of scaling every single character we will scale line in opposite direction var lineWidth: number = aWidth / scale; // result var mLineStart: number[] = []; var mLineChars: number[] = []; var mPageStart: number[] = []; var mMaxLine: number = 0; var firstLineOnPage: boolean = true; var pageCounter: number = 0; // char position in text var currentPosition: number = 0; // first line position mLineStart[mMaxLine] = currentPosition; // first page mPageStart[pageCounter++] = 0; // remaining height of current page var remainingHeight: number = aHeight; // whole text while (TextWrapper.hasNext()) { var charCount: number = 0; // saves number of chars before last space var saveSpaceCharCount:number = 0; var saveCharPosition:number = -1; // (previous) type of character var type: eCharType = eCharType.UNDEFINED; var previousType: eCharType = eCharType.UNDEFINED; // remaining width will decrease with words read var remainingWidth: number = lineWidth; // previous char code var prevCharCode: number = -1; // single line while (TextWrapper.hasNext()) { currentPosition = TextWrapper.getPosition(); // read char and move in text by 1 character forward var char: string = TextWrapper.getChar(); // get type and code type = TextWrapper.getCharType(char); var charCode: number = char.charCodeAt(0); // process based on type if (type === eCharType.SPACE) { if (previousType != eCharType.SPACE) saveSpaceCharCount = charCount; ++charCount; remainingWidth -= TextWrapper.getCharAdvance(charCode, prevCharCode); } else if (type === eCharType.CHARACTER) { if (previousType !== eCharType.CHARACTER) saveCharPosition = currentPosition; remainingWidth -= TextWrapper.getCharAdvance(charCode, prevCharCode); if (remainingWidth < 0) break; ++charCount; } else if (type === eCharType.NEWLINE) { var breakLoop: boolean = false; // if there is no more text then ignore new line if (TextWrapper.hasNext()) { breakLoop = true; saveSpaceCharCount = charCount; saveCharPosition = TextWrapper.getPosition(); currentPosition = saveCharPosition; // simulate normal width overflow remainingWidth = -1; type = eCharType.CHARACTER; } if (breakLoop) break; } previousType = type; prevCharCode = charCode; } // lines / pages remainingHeight -= lineHeight; // set new page if not enough remaining height if (remainingHeight < 0) mPageStart[pageCounter++] = mMaxLine; if (remainingWidth < 0 && type === eCharType.CHARACTER) { if (saveSpaceCharCount != 0) mLineChars[mMaxLine] = saveSpaceCharCount; else // for too long words that do not fit into one line (and Chinese texts) mLineChars[mMaxLine] = charCount; // does new line still fits into current page? firstLineOnPage = false; // set new page if (remainingHeight < 0) { firstLineOnPage = true; remainingHeight = aHeight - lineHeight; } if (saveSpaceCharCount != 0) { mLineStart[++mMaxLine] = saveCharPosition; TextWrapper.setPosition(saveCharPosition); } else { mLineStart[++mMaxLine] = currentPosition; TextWrapper.setPosition(currentPosition); } } else if (!TextWrapper.hasNext()) { if (type === eCharType.CHARACTER) { mLineChars[mMaxLine] = charCount; } else if (type === eCharType.SPACE) { mLineChars[mMaxLine] = saveSpaceCharCount; } } } mPageStart[pageCounter] = mMaxLine + 1; // lines into string[] var result: string[] = []; for (var i = 1; i <= pageCounter; i++) { var firstLine: number = mPageStart[i - 1]; var lastLine: number = mPageStart[i]; var pageText: string[] = []; for (var l = firstLine; l < lastLine; l++) { pageText.push(TextWrapper.mText.substr(mLineStart[l], mLineChars[l])); } result.push(pageText.join("\n")); } return result; } } }
You can download the example with Typescript source files here.
By the way, in last days we released our new mobile game "Flat Jewels Match 3" for iOS and Android - give it try!
great!
ReplyDelete