Multi screen game development in Phaser

Note: This tutorial was targeted for a previous version of the Phaser library. The updated version can be found here.

Phaser is a robust, famous multi-platform game development framework based on the fast PIXI library. It is getting much attention now-a-days. There are so much examples there in http://examples.phaser.io/, and thus learning Phaser is very simple.

First of all, thanks for the team behind this excellent library. When we consider multi-platform development, we need to think about multiple resolutions and devices. Using high quality assets in big screen devices will ensure your game will look beautifully on those. But this high quality assets will affect performance of low end devices, so we need to load the graphics for each devices separately and scale up or down the game according to the screen resolution.

We are considering five screen sizes;

Small – 360x240
Normal – 480x320
Large – 720x480
XLarge – 960x640
XXLarge – 1440x960

First, we need to create assets for those screens. In this example, we are going to create a bg image to show how it will appear on all resolutions. You can use any asset resizer for this purpose, but make sure the bg dimensions are exactly the above values for each one. I used this one https://github.com/asystat/Final-Android-Resizer

In phaser, there are three scaling supports namely – NO_SCALE, EXACT_FIT and SHOW_ALL. We are missing NO_BORDER scaling which we need to display our game nicely on all screens without the black borders. So for example, for a 800×480 device, the game loads ‘large’ asset and uses 720×480 resolution and scales the canvas up to fill the screen.

Also Read:   Animated particles in Phaser

We are going to modify the FullScreen Mobile Template in the phaser repo. We need to decide on what will be our game logic width and logic height. That is, if a sprite is positioned horizontally on logicWidth it should display on right edge. In this example, we take 480×320 as logicWidth and logicHeight respectively. Based on this aspect ratio, we will scale up/down the gameWidth and gameHeight.

(function () {
 //By default we set 
 BasicGame.screen = "small";
 BasicGame.srx = Math.max(window.innerWidth,window.innerHeight);
 BasicGame.sry = Math.min(window.innerWidth,window.innerHeight);
 
 BasicGame.logicWidth = 480;
 BasicGame.logicHeight = 320;
 var r = BasicGame.logicWidth/BasicGame.logicHeight;

 if(BasicGame.srx >= 360){
  BasicGame.screen = "small";
  BasicGame.gameWidth = 360;
 }
 if(BasicGame.srx >= 480){
  BasicGame.screen = "normal";
  BasicGame.gameWidth = 480;
 }
 if(BasicGame.srx >= 720){
  BasicGame.screen = "large";
  BasicGame.gameWidth = 720;
 }
 if(BasicGame.srx >= 960){
  BasicGame.screen = "xlarge";
  BasicGame.gameWidth = 960;
 }
 if(BasicGame.srx >= 1440){
  BasicGame.screen = "xxlarge";
  BasicGame.gameWidth = 1440;
 }
 
 //If on deskop, we may need to fix the maximum resolution instead of scaling the game to the full monitor resolution
 var device = new Phaser.Device();
 if(device.desktop){
  BasicGame.screen = "large";
  BasicGame.gameWidth = 720;
 }
 device = null;
 
 
 BasicGame.gameHeight = BasicGame.gameWidth/r;
 //We need these methods later to convert the logical game position to display position, So convertWidth(logicWidth) will be right edge for all screens
 BasicGame.convertWidth = function(value){
  return value/BasicGame.logicWidth * BasicGame.gameWidth; 
 };
 BasicGame.convertHeight = function(value){
  return value/BasicGame.logicHeight * BasicGame.gameHeight;
 };
 
 var game = new Phaser.Game(BasicGame.gameWidth,BasicGame.gameHeight, Phaser.AUTO, 'game');

 // Add the States your game has.
 // You don't have to do this in the html, it could be done in your Boot state too, but for simplicity I'll keep it here.
 game.state.add('Boot', BasicGame.Boot);
 game.state.add('Preloader', BasicGame.Preloader);
 game.state.add('MainMenu', BasicGame.MainMenu);
 game.state.add('Game', BasicGame.Game);

 // Now start the Boot state.
 game.state.start('Boot');

})();

In the Boot.js, create a method scaleStage and add this code and call this inside create() and leaveIncorrectOrientation() methods,

scaleStage:function(){
     if (this.game.device.desktop)
        {
            this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL; 
        }
        else
        {
            this.scale.scaleMode = Phaser.ScaleManager.NO_BORDER;
            this.scale.forceOrientation(true, false);
            this.scale.hasResized.add(this.gameResized, this);
            this.scale.enterIncorrectOrientation.add(this.enterIncorrectOrientation, this);
            this.scale.leaveIncorrectOrientation.add(this.leaveIncorrectOrientation, this);
            this.scale.setScreenSize(true);
        }
        
        this.scale.minWidth = BasicGame.gameWidth/2;
        this.scale.minHeight = BasicGame.gameHeight/2;
        this.scale.maxWidth = BasicGame.gameWidth;
        this.scale.maxHeight = BasicGame.gameHeight;
        this.scale.pageAlignHorizontally = true;
        this.scale.pageAlignVertically = true;
        this.scale.setScreenSize(true);
        
  if(this.scale.scaleMode==Phaser.ScaleManager.NO_BORDER){
   BasicGame.viewX = (this.scale.width/2 - window.innerWidth/2)*this.scale.scaleFactor.x;
   BasicGame.viewY = (this.scale.height/2 - window.innerHeight/2 - 1)*this.scale.scaleFactor.y;
   BasicGame.viewWidth = BasicGame.gameWidth-BasicGame.viewX;
   BasicGame.viewHeight = BasicGame.gameHeight-BasicGame.viewY;
  }else{
   BasicGame.viewX = 0;
   BasicGame.viewY = 0;
   BasicGame.viewWidth = BasicGame.gameWidth;
   BasicGame.viewHeight = BasicGame.gameHeight;
  }
 
  document.getElementById("game").style.width = window.innerWidth+"px";
  document.getElementById("game").style.height = window.innerHeight-1+"px";//The css for body includes 1px top margin, I believe this is the cause for this -1
  document.getElementById("game").style.overflow = "hidden";
    },

We set SHOW_ALL for desktop browsers and NO_BORDER for mobile devices. Of course, you can change this to your preference.

Also Read:   Godot Engine game tutorial for beginners - Create a 2D racing game part 5

There are four parameters defined – viewX, viewY,viewWidth and viewHeight

These correspond to our viewing area. We will require this to position hud elements to the edges of the screen.

In the preloader, you can load assets like

this.load.image('bg','assets/'+BasicGame.screen+"/bg.jpg");
this.load.image('playBtn','assets/'+BasicGame.screen+"/playBtn.png");

Before doing all this, please use this and include ScaleManager2.js file. I have modified the scaling method in Phaser.ScaleManager to support the NO_BORDER scaling. This ensure us that mouse input clicks are positioned well and not offsetted (This happened before during testing and hence I wrote this code).

ScaleManager2.js code

/**Injecting no border code for Phaser.ScaleManager*/
Phaser.ScaleManager.prototype.NO_BORDER = 3;
Phaser.ScaleManager.prototype.setScreenSize = function (force) {
        if (typeof force == 'undefined')
        {
            force = false;
        }

        if (this.game.device.iPad === false && this.game.device.webApp === false && this.game.device.desktop === false)
        {
            if (this.game.device.android && this.game.device.chrome === false)
            {
                window.scrollTo(0, 1);
            }
            else
            {
                window.scrollTo(0, 0);
            }
        }

        this._iterations--;

        if (force || window.innerHeight > this._startHeight || this._iterations < 0)
        {
            // Set minimum height of content to new window height
            document.documentElement['style'].minHeight = window.innerHeight + 'px';

            if (this.incorrectOrientation === true)
            {
                this.setMaximum();
            }
            else if (!this.isFullScreen)
            {
                if (this.scaleMode == Phaser.ScaleManager.EXACT_FIT)
                {
                    this.setExactFit();
                }
                else if (this.scaleMode == Phaser.ScaleManager.SHOW_ALL)
                {
                    this.setShowAll();
                }
                else if(this.scaleMode == Phaser.ScaleManager.NO_BORDER)
                {
                 this.setNoBorder();//Don't call setSize
                 clearInterval(this._check);
              this._check = null;
              return;
                }
            }
            else
            {
                if (this.fullScreenScaleMode == Phaser.ScaleManager.EXACT_FIT)
                {
                    this.setExactFit();
                }
                else if (this.fullScreenScaleMode == Phaser.ScaleManager.SHOW_ALL)
                {
                    this.setShowAll();
                }
                else if(this.scaleMode == Phaser.ScaleManager.NO_BORDER)
                {
                 this.setNoBorder();//Don't call setSize
                 clearInterval(this._check);
              this._check = null;
              return;
                }
            }
            this.setSize();
            clearInterval(this._check);
            this._check = null;
        }

    }
Phaser.ScaleManager.prototype.setNoBorder = function(){
  this.setShowAll();
  var ow = parseInt(this.width,10);
  var oh = parseInt(this.height,10);
  var r = Math.max(window.innerWidth/ow,window.innerHeight/oh);
  this.width = ow*r;
  this.height = oh*r;
  this.setSize2();
}
Phaser.ScaleManager.prototype.setSize2 = function(){
        this.game.canvas.style.width = this.width + 'px';
        this.game.canvas.style.height = this.height + 'px';
        this.game.input.scale.setTo(this.game.width / this.width, this.game.height / this.height);
        if (this.pageAlignHorizontally)
        {
            if (this.incorrectOrientation === false)
            {
                this.margin.x = Math.round((window.innerWidth - this.width) / 2);
                this.game.canvas.style.marginLeft = this.margin.x + 'px';
            }
            else
            {
                this.margin.x = 0;
                this.game.canvas.style.marginLeft = '0px';
            }
        }

        if (this.pageAlignVertically)
        {
            if (this.incorrectOrientation === false)
            {
                this.margin.y = Math.round((window.innerHeight - this.height) / 2);
                this.game.canvas.style.marginTop = this.margin.y + 'px';
            }
            else
            {
                this.margin.y = 0;
                this.game.canvas.style.marginTop = '0px';
            }
        }

        Phaser.Canvas.getOffset(this.game.canvas, this.game.stage.offset);
        this.aspectRatio = this.width / this.height;
        this.scaleFactor.x = this.game.width / this.width;
        this.scaleFactor.y = this.game.height / this.height;
        this.scaleFactorInversed.x = this.width / this.game.width;
        this.scaleFactorInversed.y = this.height / this.game.height;
        this.hasResized.dispatch(this.width, this.height);
        this.checkOrientationState();
}

And remember, when positioning an object, use convertWidth and convertHeight. For HUD elements use viewX, viewY, viewWidth and viewHeight.

Also Read:   Resolution Switcher Plugin for Godot

Full code example here: link

Thanks for reading this. 😀

})();

[Total: 1    Average: 1/5]
  • This post deliver best explanation about multi screen game development. The coding which share in the post is very useful for learning the whole process.
    html5 web game development

  • You need to revert the widths and heights to work for portrait mode.
    I believe this code won't work now because this was for an older Phaser and Phaser now has custom scaling solution which I didn't try yet.

  • No, I haven't tried CocoonJS

  • I'm saving this in my bookmarks. It's an awesome tutorial, just what I was looking for!

  • cg

    Thanks for this example, it works great! I did notice a few issues that I have either not quite worked out yet, or haven't been addressed:

    – The address bar doesn't disappear on mobile browsers
    – The screen size gets determined by the portrait width when loaded in portrait mode (which will be the height of the game once it is in landscape mode) resulting in smaller assets being loaded
    – It doesn't seem to take into account Retina displays and their appropriate image density

    Do you have any advice/help on these issues?

  • Hi, first compliment for your code, I think that is a better solution to preload the right image, and will have the possibility use the fluid positioned elements.
    I tested your code example on different devices, but some case work well only if the page loaded only in landscape mode, or if i did the refresh on the page.
    I've uploaded also the images in phaser forum:
    http://www.html5gamedevs.com/topic/5949-solution-scaling-for-multiple-devicesresolution-and-screens/
    I proved to modify your code, but without success.
    Thanks a lot

  • Thank you, this seems to be specific and good tutorial to get for 5 different screen sizes. I was thinking would it be possible to get the scaling go automatically to all sizes and scale everything.. I will give this closer look when I get to my desktop computer.

  • Anonymous

    This is great. Thanks for this.

  • Hi,

    How to make this work with portrait mode?

  • you have a tutorial or template to integrate with CocoonJS? : D