Phaser tutorial: How to wrap bitmap text

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.

Your browser does not support iframes.

>

Using the class is as simple as this:

vartext: 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 :-)';varpages: string[]=Helper.TextWrapper.wrapText(text,520,260,'Font',25);varcurrentPage: number=0;varbitmapText=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:

moduleHelper{enumeCharType{UNDEFINED=-1,SPACE=1,NEWLINE=2,CHARACTER=3,//SPECIAL = 4 // for future}exportclassTextWrapper{staticmText: string;staticmTextPosition: number;staticmFontData: any;// -------------------------------------------------------------------------privatestatichasNext():boolean{returnTextWrapper.mTextPosition<TextWrapper.mText.length;}// -------------------------------------------------------------------------privatestaticgetChar():string{returnTextWrapper.mText.charAt(TextWrapper.mTextPosition++);}// -------------------------------------------------------------------------privatestaticpeekChar():string{returnTextWrapper.mText.charAt(TextWrapper.mTextPosition);}// -------------------------------------------------------------------------privatestaticgetPosition():number{returnTextWrapper.mTextPosition;}// -------------------------------------------------------------------------privatestaticsetPosition(aPosition: number):void{TextWrapper.mTextPosition=aPosition;}// -------------------------------------------------------------------------privatestaticgetCharAdvance(aCharCode: number,aPrevCharCode: number):number{varcharData=TextWrapper.mFontData.chars[aCharCode];// widthvaradvance: number=charData.xAdvance;// kerningif(aPrevCharCode>0&&charData.kerning[aPrevCharCode])advance+=charData.kerning[aPrevCharCode];returnadvance;}// -------------------------------------------------------------------------privatestaticgetCharType(aChar: string):eCharType{if(aChar===' ')returneCharType.SPACE;elseif(/(?:\r\n|\r|\n)/.test(aChar))returneCharType.NEWLINE;elsereturneCharType.CHARACTER;}// -------------------------------------------------------------------------staticwrapText(aText: string,aWidth: number,aHeight:number,aFontName: string,aSize? : number):string[]{// set vars for text processingTextWrapper.mText=aText;TextWrapper.setPosition(0);// font dataTextWrapper.mFontData=PIXI.BitmapText.fonts[aFontName];// if size not defined then take default sizeif(aSize===undefined)aSize=TextWrapper.mFontData.size;varscale: number=aSize/TextWrapper.mFontData.size;// height of line scaledvarlineHeight: number=TextWrapper.mFontData.lineHeight*scale;// instead of scaling every single character we will scale line in opposite directionvarlineWidth: number=aWidth/scale;// resultvarmLineStart: number[]=[];varmLineChars: number[]=[];varmPageStart: number[]=[];varmMaxLine: number=0;varfirstLineOnPage: boolean=true;varpageCounter: number=0;// char position in textvarcurrentPosition: number=0;// first line positionmLineStart[mMaxLine]=currentPosition;// first pagemPageStart[pageCounter++]=0;// remaining height of current pagevarremainingHeight: number=aHeight;// whole textwhile(TextWrapper.hasNext()){varcharCount: number=0;// saves number of chars before last spacevarsaveSpaceCharCount:number=0;varsaveCharPosition:number=-1;// (previous) type of charactervartype: eCharType=eCharType.UNDEFINED;varpreviousType: eCharType=eCharType.UNDEFINED;// remaining width will decrease with words readvarremainingWidth: number=lineWidth;// previous char codevarprevCharCode: number=-1;// single linewhile(TextWrapper.hasNext()){currentPosition=TextWrapper.getPosition();// read char and move in text by 1 character forwardvarchar:string=TextWrapper.getChar();// get type and codetype=TextWrapper.getCharType(char);varcharCode: number=char.charCodeAt(0);// process based on typeif(type===eCharType.SPACE){if(previousType!=eCharType.SPACE)saveSpaceCharCount=charCount;++charCount;remainingWidth-=TextWrapper.getCharAdvance(charCode,prevCharCode);}elseif(type===eCharType.CHARACTER){if(previousType!==eCharType.CHARACTER)saveCharPosition=currentPosition;remainingWidth-=TextWrapper.getCharAdvance(charCode,prevCharCode);if(remainingWidth<0)break;++charCount;}elseif(type===eCharType.NEWLINE){varbreakLoop: boolean=false;// if there is no more text then ignore new lineif(TextWrapper.hasNext()){breakLoop=true;saveSpaceCharCount=charCount;saveCharPosition=TextWrapper.getPosition();currentPosition=saveCharPosition;// simulate normal width overflowremainingWidth=-1;type=eCharType.CHARACTER;}if(breakLoop)break;}previousType=type;prevCharCode=charCode;}// lines / pagesremainingHeight-=lineHeight;// set new page if not enough remaining heightif(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 pageif(remainingHeight<0){firstLineOnPage=true;remainingHeight=aHeight-lineHeight;}if(saveSpaceCharCount!=0){mLineStart[++mMaxLine]=saveCharPosition;TextWrapper.setPosition(saveCharPosition);}else{mLineStart[++mMaxLine]=currentPosition;TextWrapper.setPosition(currentPosition);}}elseif(!TextWrapper.hasNext()){if(type===eCharType.CHARACTER){mLineChars[mMaxLine]=charCount;}elseif(type===eCharType.SPACE){mLineChars[mMaxLine]=saveSpaceCharCount;}}}mPageStart[pageCounter]=mMaxLine+1;// lines into string[]varresult: string[]=[];for(vari=1;i<=pageCounter;i++){varfirstLine: number=mPageStart[i-1];varlastLine: number=mPageStart[i];varpageText: string[]=[];for(varl=firstLine;l<lastLine;l++){pageText.push(TextWrapper.mText.substr(mLineStart[l],mLineChars[l]));}result.push(pageText.join("\n"));}returnresult;}}}