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:   Drawing a circle using Gizmos in Unity3D

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:   Getting Started in Phaser 3 (ES6) - Create a Boomdots game

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:   Getting Started in Phaser 3 (ES6) – Create a Boomdots game part 2

Full code example here: link

Thanks for reading this. 😀

})();

[Total: 2    Average: 3/5]