Thursday, April 9, 2015

Phaser tutorial: manage different screen sizes






 Handling various screen resolutions for mobile game is frequent topic. You have finnished your great new game and suddenly lot of problems appear. Part of game is not visible here or it is ugly stretched there or there are some ugly black belts. It is best to think about handling this already in the beginning of development.
 In this tutorial I will present method that works for me and is quite easy to implement. I will use Phaser engine - great engine by Rich Davey from Photonstorm.com. In the bottom you will also find link to test project.


Solution requirements

 My requirements for final solution were:
  • no black belts,
  • avoid stretching as much as possible,
  • universal solution I can use in future games (both portrait or landscape).
 In past when I was handling different resolutions there were two edge cases: iPhone which is long and narrow and iPad which is short and wide. I will take these two devices as limits, but the solution will handle also resolutions beyond them.


Creating test image

 First, let's decide on our default resolution. I will set it to 1280 x 800. Later we will see, we are not fixed to this. But what is important is, that aspect ratio for this resolution is 1.6. Let's create test image with this resolution like this:

   As far as we are displaying this on devices with aspect 1.6, we are happy. Now, what happens if we want to display this image on iPhone? iPhone 4 has resolution 1136 x 640. If we scale above image down proportionally, saving aspect ratio then it will shrink to 1024 x 640, so we are missing 112 pixels (1136 - 1024). As a result we have black belts on each side, each 56 pixels wide. Or we can stretch the image, but then we are sacrificing aspect and it does not look good.
 We can also go in opposite direction and ask: how big would be iPhone full screen image if we wanted to scale it so, that height is 800? Answer is: it has to be scaled from 1136 x 640 to 1420 x 800. So, let's adjust our test image like this:


  If we now scale this image down from height 800 to 640 while preserving aspect we will go from 1420 to 1136 in width. On iPhone you will see additional space. This additional space is 70 pixels on image, but as it is scaled down on iPhone it will be only 56 pixels on real screen.
 Now, let's do the same for iPad resolution. It is 1024 x 768 in landscape. To fill the width with green part of our image (1280 pixels) we have to scale it down to 1024 (80%). To preserve aspect it means we also have to scale down height by 80% from 800 to 640. We ended with black belts again! This time they are horizontal in top and bottom in total width 768 - 640 = 128 pixel (64 pixels each).
 If we go in opposite direction (how high the image has to be to cover whole screen without stretching?) we end with resolution 1280 x 960. This scaled down by 80% will give us 1024 x 768. Test image for iPad looks like this:


 In next step we will merge both test images. I added circle somewhere in the middle. It will visually indicate whether image is stretched or not. Final test image is like this:



Game images

 We have test image. With it we can handle any device with aspect from iPhone's 1.775 (1136 / 640) through default 1.6 (1280 / 800) to iPad's 1.333. If we get device with aspect over 1.775 or below 1.333 we will stretch the final image to avoid black belts. For example, device will have resolution 1920 x 1080. It is aspect 1.777, so we will have to stretch the image a little. But it is already visually hardly noticeable.

 The grey parts of test image will be never displayed on any resolution. In every case only the blue part or red part will be displayed ... or none of them if aspect is 1.6.

 So it is good to design your full screen backgrounds like this and place some non-important content into blue and red areas as it may be displayed full, partly or not at all. Just for an example, look at this island image (by Tomas Kopecky from littlecolor.com) with test image over it:


 The game will take part in the middle area. Blue and red areas will not be visible on some devices.


Changing default resolution

 I think it is clear from the discussion, that aspect ratios are more important then absolute width and height. So, we can leave default resolution, as 1280 x 800 is still quite a big for HTML5 game. Let's say we start new game and we are deciding on resolution. As far as our new default resolution will keep aspect 1.6 we are OK. So, we will choose default resolution to 800 x 500. It is 62.5% of 1280 x 800. We can easily scale down whole test image with this value and we will get template for our new default resolution (of course, this will not recalculate or rewrite numbers on the image - there will be still 1280, 800, ...!).
 The blue areas will not be 70 pixels on each side more, but 70 * 62.5% = 44 pixels (rounded up). And red areas will shrink from 80 pixels in height to 80 * 62.5% = 50 pixels.


Let's write some code!

 It is time to write some code. I will use Visual Studio and typescript. I will not show here adjustment in index.html and app.css file as you can see it in final project.
 After you create new project, go to app.ts and replace all generated content with this:

module Scaler {
    export class Globals {
        // game derived from Phaser.Game
        static game: Game = null;

        // game orientation
        static correctOrientation: boolean = false;
    }
}

// -------------------------------------------------------------------------
window.onload = () => {
    Scaler.Globals.game = new Scaler.Game();
};

 This will call our Game class which is derived from Phaser.Game. Add game.ts file and put this into it:

module Scaler {

    export class Game extends Phaser.Game {
        // -------------------------------------------------------------------------
        constructor() {
            var screenDims = Utils.ScreenUtils.calculateScreenMetrics(800, 500,
                Utils.Orientation.LANDSCAPE);

            super(screenDims.gameWidth, screenDims.gameHeight, Phaser.AUTO, "content",
                null /* , transparent, antialias, physicsConfig */);
            
            // states
            this.state.add('Boot', Boot);

            // start
            this.state.start('Boot');
        }
    }
}

 In constructor we are calling static method calculateScreenMetrics() in ScreenUtils class which is inside Utils module. This method takes default resolution for game as well as enum value which says whether the game will run in landscape or portrait. The returned object contains lot of useful information:
  • windowWidth, windowHeight - dims of window the game will run in,
  • defaultGameWidth, defaultGameHeight - our default resolution we passed into calculation,
  • maxGameWidth, maxGameHeight - we can pass these values or it will be calculated for us in the method. If calculated by method then pixel limits are calculated from iPhone to iPad. In other words: if we pass as default 1280 x 800, the values calculated here will be 1420 x 960 (which is dimension of our original test image). If we pass 800 x 500 then result is 888 x 600,
  • gameWidth, gameHeight - values to pass to Phaser.Game constructor - dimensions of game before scaling to fill window,
  • scaleX, scaleY - how much to scale along x and y to fill window. As far as the window dimension is between iPhone and iPad aspect the scaling will be uniform,
  • offsetX, offsetY - how many pixels (in the game dimensions) from the top left corner the central area starts. Actually, offsetX is width of left blue area and offsetY is height of top red area on the screen. As only red or blue (or none) area is displayed, one or both of the values will always be zero.
 Now, we can call parent (Phaser.Game) constructor with calculated values for gameWidth and gameHeight. If window has aspect 1.6 then it will be called with default values.

 In this example I am adding only one state, that will set ScaleManager values and load our test image. Add boot.ts file:

module Scaler {

    export class Boot extends Phaser.State {
        // -------------------------------------------------------------------------
        constructor() {
            super();
        }

        // -------------------------------------------------------------------------
        init() {
            this.input.maxPointers = 1;
            this.stage.disableVisibilityChange = false;

            var screenDims = Utils.ScreenUtils.screenMetrics;

            if (this.game.device.desktop) {
                console.log("DESKTOP");
                this.scale.scaleMode = Phaser.ScaleManager.USER_SCALE;
                this.scale.setUserScale(screenDims.scaleX, screenDims.scaleY);
                this.scale.pageAlignHorizontally = true;
                this.scale.pageAlignVertically = true;
            }
            else {
                console.log("MOBILE");
                this.scale.scaleMode = Phaser.ScaleManager.USER_SCALE;
                this.scale.setUserScale(screenDims.scaleX, screenDims.scaleY);
                this.scale.pageAlignHorizontally = true;
                this.scale.pageAlignVertically = true;
                this.scale.forceOrientation(true, false);
            }

            console.log(screenDims);
        } 

 In init() method we are using Phaser's USER_SCALE method for scaling game to window. We also use here scaleX and scaleY values we previously calculated. The rest of the boot.ts file just loads test image and displays it:

        // -------------------------------------------------------------------------
        preload() {
            this.load.image("Test", "assets/test.png");
        }

        // -------------------------------------------------------------------------
        create() {
            this.stage.backgroundColor = 0x8080FF;

            var bg: Phaser.Image = this.add.sprite(this.world.centerX, this.world.centerY, "Test");
            bg.anchor.setTo(0.5, 0.5);
        }
    }
}


ScreenUtils

 Most important piece of code remains. Our reusable class for calculating screen metrics. Add screenutils.ts file. It starts with ScreenMetrics class. I already described all members above, so I will not repeat it here, only show listing:

module Utils {

    export class ScreenMetrics {
        windowWidth: number;
        windowHeight: number;

        defaultGameWidth: number;
        defaultGameHeight: number;
        maxGameWidth: number;
        maxGameHeight: number

        gameWidth: number;
        gameHeight: number;
        scaleX: number;
        scaleY: number;
        offsetX: number;
        offsetY: number;
    }

 Next definition of enum, that says whether game is in portrait or landscape follows:

    export enum Orientation {PORTRAIT, LANDSCAPE };

 And finally class ScreenUtils that contains calculateScreenMetrics() method:

    export class ScreenUtils {
        public static screenMetrics: ScreenMetrics;

        // -------------------------------------------------------------------------
        public static calculateScreenMetrics(aDefaultWidth: number, aDefaultHeight: number,
            aOrientation: Orientation = Orientation.LANDSCAPE,
            aMaxGameWidth?: number, aMaxGameHeight?: number): ScreenMetrics {

 First, we take window dimensions from browser:

            // get dimension of window
            var windowWidth: number = window.innerWidth;
            var windowHeight: number = window.innerHeight;

 Then we check if it makes sens for landscape / portrait game. For landscape the width should be higher than height. This should handle situation when landscape game starts in portrait. But I highly recommend to recalculate metrics and call ScaleManager's setGameSize() method when leaving incorrect orientation (this is not part of this example project):

            // swap if window dimensions do not match orientation
            if ((windowWidth < windowHeight && aOrientation === Orientation.LANDSCAPE) ||
                (windowHeight < windowWidth && aOrientation === Orientation.PORTRAIT)) {
                var tmp: number = windowWidth;
                windowWidth = windowHeight;
                windowHeight = tmp;
            }

  After that we check if optional parameters on max width and height were provided. If not, we will calculate it by ourselves. Here are absolute values from our original test image used. But only as ratios to calculate how much to prolong default width for iPhone and default height for iPad:

            // calculate max game dimension. The bounds are iPad and iPhone 
            if (typeof aMaxGameWidth === "undefined" || typeof aMaxGameHeight === "undefined") {
                if (aOrientation === Orientation.LANDSCAPE) {
                    aMaxGameWidth = Math.round(aDefaultWidth * 1420 / 1280);
                    aMaxGameHeight = Math.round(aDefaultHeight * 960 / 800);
                } else {
                    aMaxGameWidth = Math.round(aDefaultWidth * 960 / 800);
                    aMaxGameHeight = Math.round(aDefaultHeight * 1420 / 1280);
                }
            }

 Here comes calculation of game width and height and also calculation of offset in case blue or red area is visible (if window aspect differs from 1.6). Part of my original code is commented out as it turned to be duplication of code in the end:

            // default aspect and current window aspect
            var defaultAspect: number = (aOrientation === Orientation.LANDSCAPE) ? 1280 / 800 : 800 / 1280;
            var windowAspect: number = windowWidth / windowHeight;

            var offsetX: number = 0;
            var offsetY: number = 0;
            var gameWidth: number = 0;
            var gameHeight: number = 0;

            // if (aOrientation === Orientation.LANDSCAPE) {
                // "iPhone" landscape ... and "iPad" portrait
                if (windowAspect > defaultAspect) {
                    gameHeight = aDefaultHeight;
                    gameWidth = Math.ceil((gameHeight * windowAspect) / 2.0) * 2;
                    gameWidth = Math.min(gameWidth, aMaxGameWidth);
                    offsetX = (gameWidth - aDefaultWidth) / 2;
                    offsetY = 0;
                } else {    // "iPad" landscpae ... and "iPhone" portrait
                    gameWidth = aDefaultWidth;
                    gameHeight = Math.ceil((gameWidth / windowAspect) / 2.0) * 2;
                    gameHeight = Math.min(gameHeight, aMaxGameHeight);
                    offsetX = 0;
                    offsetY = (gameHeight - aDefaultHeight) / 2;
                }
            /* } else {    // "iPhone" portrait
                if (windowAspect < defaultAspect) {
                    gameWidth = aDefaultWidth;
                    gameHeight = gameWidth / windowAspect;
                    gameHeight = Math.min(gameHeight, aMaxGameHeight);
                    offsetX = 0;
                    offsetY = (gameHeight - aDefaultHeight) / 2;
                } else {    // "iPad" portrait
                    gameHeight = aDefaultHeight;
                    gameWidth = gameHeight = windowAspect;
                    gameWidth = Math.min(gameWidth, aMaxGameWidth);
                    offsetX = (gameWidth - aDefaultWidth) / 2;
                    offsetY = 0;
                }
            }
            */

 Last values we have to calculate are scale values for ScaleManger:

            // calculate scale
            var scaleX: number = windowWidth / gameWidth;
            var scaleY: number = windowHeight / gameHeight;

 Finally, we store all values for later use and return result:

            // store values
            this.screenMetrics = new ScreenMetrics();
            this.screenMetrics.windowWidth = windowWidth;
            this.screenMetrics.windowHeight = windowHeight;

            this.screenMetrics.defaultGameWidth = aDefaultWidth;
            this.screenMetrics.defaultGameHeight = aDefaultHeight;
            this.screenMetrics.maxGameWidth = aMaxGameWidth;
            this.screenMetrics.maxGameHeight = aMaxGameHeight;
            
            this.screenMetrics.gameWidth = gameWidth;
            this.screenMetrics.gameHeight = gameHeight;
            this.screenMetrics.scaleX = scaleX;
            this.screenMetrics.scaleY = scaleY;
            this.screenMetrics.offsetX = offsetX;
            this.screenMetrics.offsetY = offsetY;

            return this.screenMetrics;
        }
    }
}


Result

 We are finished. Let's see it in work. In Firefox if you press ctrl+shift+I you will open debug tools window. Locate this icon and press it:


 You can now change window width and height. Press F5 each time you want the game start again and recalculate the screen metrics. For example: if you set the window to 800 x 500 (our default size) and press F5 you will get this image:


 Do not get confused with numbers on the image. We first created test image with 1280 x 800 in this article. But then we decided on default resolution 800 x 500 for new game and scaled test image 62.5% down. Of course, the scaling did not overwrote original numbers on the image for us! Green area is 800 x 500 (see test.png image in project's assets folder).

 You see only inner green part as we used dimensions with 1.6 aspect. You will get the same result for 1600 x 1000, but this time the game scale values for x and y will be 2.0. Now expand the window horizontally to 888 x 500. For this resolution you will get maximum of blue areas and this resolution has aspect of iPhone (888 / 500 = 1136 / 640 = 1.775) ... ok, there is small rounding 1.776 vs 1.775 as ScreenUtils is setting size to whole numbers and multiplies of 2. But, no stretching! No black bars!


  Now, expand window more to 1200 x 500. Aspect 2.4 is over max supported aspect (1.775 for iPhone). As we do not have any additional graphics beyond blue areas, we have to stretch horizontally. Notice, that this resolution is extreme and was used here just for testing purpose.


 You can also play with height. Set dimensions to 800 x 600. It is iPad aspect and full red areas are visible. Adding more height would result in vertical stretching.



Conclusion

 Hope, this tutorial helped you to tame different resolutions for your game. Here you can download whole project: Scaler.zip







Saturday, April 4, 2015

Shards - the Brick Breaker Lite is available at iTunes!







 Shards - the Brick Breaker Lite is finally available for download at iTunes!

 If you want to play modern arkanoid / brickbreaker clone go and get it. Game has 70 levels, each with unique fractal background. Glassy bricks has arbitrary scale and rotate. Soundtrack is simply great!






Saturday, March 28, 2015

Unity: Two flavours of LERP





 In this post I will show two uses of LERP as you can often see in Unity scripts.


LERP


 LERP stands for linear interpolation and it is easy way how to get "mid-points" if you need to travel from point A to point B. Let's say you need to get from point A to point B in 3 seconds. We will call these 3 seconds as "MOVEMENT_TIME". You set up some counter "_currentTime" that will increase every frame with elapsed time until you reach MOVEMENT_TIME. Your current position on every frame is then calculated like this:

newPosition = point A + (point B - point A) * (_curerntTime / MOVEMENT_TIME)

 What it does is:
  • elapsed time is changed into range 0 to 1 (_curerntTime / MOVEMENT_TIME),
  • distance between ending and starting point (point B - point A) is calculated,
  • these two values are multiplied to know how much of the distance we already traveled (if time is 0 we are still in beginning and if time is 1 we are in target position),
  • finally we add the distance to starting position (point A) to get current position
 As this is used very frequently Unity has Lerp function in its Mathf class. It simplifies whole thing to single call to:

float newValue = Mathf.Lerp(valueA, valueB, time);

 But points are not single values! Fortunately, Unity has whole bunch of Lerp functions for Vector2, Vector3, Color, ... Our position change would look like this in code:

Vector2 newPosition = Vector2.Lerp(PointA, PointB, _currentTime / MOVEMENT_TIME);



Two common usages of LERP


 Now look at following picture:

 Both, ship and zeppelin are using LERP for its movement. But, it is obvious that zeppelin is not moving linearly.

 The ship is using our previous formula: we have starting point, end point and every frame we are looking for current point between them. Here is chart with our x position with regard to time:


 Here is complete code for ship:

using UnityEngine;
using System.Collections;

public class Ship : MonoBehaviour {

    public const float MOVEMENT_TIME = 3f;

    private float _startX;
    private float _endX;
    private float _currentTime;

    void Start() {
        _startX = -1f;
        _endX = 1f;
        _currentTime = 0;
    }

    void Update() {
        // update time
        _currentTime = Mathf.Min(_currentTime + Time.deltaTime, MOVEMENT_TIME);

        // update position
        Vector3 newPosition = transform.position;
        newPosition.x = Mathf.Lerp(_startX, _endX, _currentTime / MOVEMENT_TIME);
        transform.position = newPosition;

        // check if already in destination
        if (_currentTime >= MOVEMENT_TIME) {
            // move again, but switch start and destination
            float tmpX = _startX;
            _startX = _endX;
            _endX = tmpX;

            // init timer again
            _currentTime = 0;

            // turn ship to head in right direction
            Vector3 newScale = transform.localScale;
            newScale.x *= -1;
            transform.localScale = newScale;
        }
    }
}


 Zeppelin is moving also using LERP, but instead of interpolating between start and end position, it is interpolating between current and end position. It also does not calculate time between 0 and 1. Instead it uses fixed value. This fixed value says how much of the remaining distance (from current position to target position) should be traveled. As the remaining distance is getting shorter and shorter then also the calculated distance to move in every frame is shorter and shorter, so the zeppelin is slowing down. Exactly, it does never reach target position, but it gets so close to it that this is not problem for us. The chart for movement looks like this:


 And here is complete code for zeppelin:

using UnityEngine;
using System.Collections;

public class Zeppelin : MonoBehaviour {

    public const float MOVEMENT_TIME = 3f;

    private float _endX;

    void Start() {
        _endX = 1f;
        StartCoroutine(InfiniteSwap());
    }

    void Update() {
        // update position
        Vector3 newPosition = transform.position;
        newPosition.x = Mathf.Lerp(transform.position.x, _endX, MOVEMENT_TIME * 0.5f * Time.deltaTime);
        transform.position = newPosition;
    }

    IEnumerator InfiniteSwap() {
        while (true) {
            // suspend for 3 seconds
            yield return new WaitForSeconds(MOVEMENT_TIME);

            // change end position
            _endX *= -1;

            // turn zeppelin to head in right direction
            Vector3 newScale = transform.localScale;
            newScale.x *= -1;
            transform.localScale = newScale;
        }
    }
}

 We are using coroutine here to change target position every 3 seconds. In both cases we are moving form -1 to 1 in x direction.

 The second way of using Lerp has lot of usages. For example if you want your camera move from player to focus on some object, it looks much better if you do it in this way. In Unity example projects you can see this for camera movement in Survival shooter or for alarm lights intensity in Stealth project.



Friday, March 6, 2015

Mobile Income Report #8 - January & February 2015





previous parts
  Mobile Income Report #7 - December 2014
  Mobile Income Report #6 - November 2014
  Mobile Income Report #5 - October 2014 
  Mobile Income Report #4 - September 2014 
  Mobile Income Report #3 - August 2014
  Mobile Income Report #2 - July 2014
  Mobile Income Report #1 - June 2014
  Apps page - Portfolio (what are my assets?)


 If you do not want to miss any of my Income Reports you can follow me on Twitter. Just click the button above.

 Under Apps page you can see my portfolio. It is list of the games that are then reported in Income Reports. The portfolio is regularly updated with new information (releases for new platforms, new updates, ...)



 As there were no interesting events in January I merged January and February report together.

What I did in January and February

  • Flat Jewels Match 3 is finished. In February we released iOS version at iTunes. Unfortunately, number of downloads is low. As we passed Amazon's developer select program with Android version we will have advertising campaign with 500k impressions in March for free,
  • on 27th January Shards was promoted by AppGratis on Android. We finally crossed 50k downloads at Google Play,
  • in the end of February we finished our first HTML5 game Annihilate. You can play it right here. Game is build with Phaser engine. Now we are looking for sponsors. Read whole post on Annihilate here. Annihilate is simple puzzle game in modern design. It scales well on almost any resolution:



Report

 

 First numbers for January. Here are paid apps:

 In January Shards dominates. In the beginning of the month there was positive effect of promotion of iOS version by AppGratis which took place on 31th December 2014 and promotion of Android version on 27th January.

 Here are versions supported with ads for January:

 In January income from ads was really poor. Chartboost was only $39 compared to $61 in December, but it still stays the most profitable ads network for us. Admob earnings are getting lower and lower with the same traffic. In the end of January Flat Jewels Match 3 was released for Android and you can see first $2 from it :-)


 In February paid apps did like this:

 Decrease compared to January (from $76 to $37,5). Almost all earnings are from Shards for Android and iOS.

 Fortunately, income from free versions increased little (from $84,2 to $128,5) to compensate decrease from paid apps:

 Surprise for us are earnings from Chartboost for Flat Jewels Match 3. Despite lower number of downloads compared to Shards, the earnings are almost the same. Now the question is: did we something wrong with Shards? Or did we something good with Flat Jewels Match 3? Flat Jewels Match 3 is also our first free app for iOS.

 Total income for January was $76,0 + $84,2 = $160,2 which is +3,9% increase compared to December.

 Total income for February was $37,5 + $128,5 = $166,0 which is another +3,6% increase compared to January... All I need now is to increase the base for calculation :-)



Next

 

 Next month I will report our experience with looking for sponsors for our first HTML5 game Annihilate. We are working on next HTML5 game.
 Also I invested some money into tutorials by 3DBuzz on new Unity UI. These tutorials are really great and I hope it will help me to start our first own Unity game. We already have clear idea and game design.






Sunday, March 1, 2015

Annihilate, puzzle game made with Phaser engine, is finshed!


 Finally, we just finshed our HTML5 game called Annihilate. Game was made using great Phaser engine. Annihilate is puzzle game in which you are matching pairs o atoms. In 48 levels you have to avoid various pitfalls like lasers, black holes, ...

 Now, we are looking for sponsors. If you are interested, conntact me at my gmail address: tomas[dot]rychnovsky[at]gmail[dot]com

 Game features:
  • 48 puzzle levels,
  • nice simple and clear graphics and music,
  • scales well for different screen resolutions (no black belts on iPhone nor iPad),
  • ready for easy translation



 And you can try the game right here!:



 Screenshots:







Tuesday, February 10, 2015

Phaser tutorial: How to wrap bitmap text





 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!





Friday, January 30, 2015

Mobile Income Report #7 - December 2014 (monthly income report from Android, iOS, Tizen, ... games)





previous parts
  Mobile Income Report #6 - November 2014
  Mobile Income Report #5 - October 2014

  Mobile Income Report #4 - September 2014 
  Mobile Income Report #3 - August 2014
  Mobile Income Report #2 - July 2014
  Mobile Income Report #1 - June 2014
  Apps page - Portfolio (what are my assets?)


 If you do not want to miss any of my Income Reports you can follow me on Twitter. Just click the button above.

 Under Apps page you can see my portfolio. It is list of the games that are then reported in Income Reports. The portfolio is regularly updated with new information (releases for new platforms, new updates, ...)


What I did in December

  • in December I continued work on our games, mainly Flat Jewels Match 3. We finished the game itself and mainly worked on additional features like various "game services" and so on. Now, when writing this post, the game is released on Android (at Google Play and at Amazon) and it is in approval process for iOS 
Flat Jewels Match 3 - menuFlat Jewels Match 3 - gameplay

  • I also started learning HTML5 and Javascript as it is whole new field of opportunities for simple games. First I used Netbeans IDE, which I really like, for my first attempts. But then I found there is Typescript language that can help with type checks and you can write code in more convenient way, so I switched to Microsoft Visual Studio Community 2013, which is free for individual developers and has Typescript support. Typescript is superset of Javascript, so everything you write is compiled into Javascript,
  • you can start to write your HTML5 game from scratch or save some time and use some engine. There is lot of them. But finally I ended with Phaser by PhotonStorm as I like their approach and it is obvious that the engine is constantly being improved,
  • On 31st December there was promotion event on Shards by AppGratis. It brought more than 17k downloads in one day and more importantly it helped us to gain some positive user ratings.

Report

 Here are December figures for paid apps:


 While income from iOS games is more or less stable (from $27 in November to $24,3), there is decrease in Android versions (from $38,1 to $22,4). Total amount $46,7 means decrease from $65,1 in November.


 Shards with 60% share is still most profitable from our games.

 Stats for free versions of our games are these:


 There is really big decrease from $220,7 in November to $107,4 in December. In November there was good result with Chartboost (close to $150), but it did not lasted long time. Also income from other networks was lower in december.


 Total income for December was $46,7 + $107,4 = $154,1 which is -46% decrease compared to November and it is really very bad result...


Next?

 In January I am continuing in work on our games - Flat Jewels Match 3 game was released on Android and is in approvel process on iOS. I hope it will help me little bit. I am also continuing in my Typescript / Phaser experiments and I hope there will be some results soon.