Scaling Games in Phaser 3 with an Alignment Grid
As game developers, one of the challenges we have faced since the invention of the smartphone is how to scale the game so it will fit on whatever device the player has. Scaling games in Phaser 3 is no exception. There are a lot of different ways programmer will achieve this. I’m going to show you the way I’ve been doing it for work and my own projects. I’m not saying it is absolutely the best way, but it works best for me. It is also an easy concept to explain because it doesn’t rely on anything built into Phaser itself, but rather simple mathematics. It will work on any platform where you can measure the widths and heights of objects and of the canvas itself. I’ll be using the Phaser 3 basic template to jump-start the project. This template has a static size on computers and a dynamic one on mobile. In other words, the canvas size matches the screen size on a phone or tablet.
This was originally a project I did for Phaser CE but I’ve made a lot of improvements since then.
How does the Alignment Grid work?
Using a dynamic canvas size means we don’t know the size of the game when we start. The alignment grid works because instead of placing an object at a static point we place it at a percentage of the canvas size. Say for example a sprite at an x position of 200 and a y position of 150, we place it at 25% of the screen’s width and 50% of the screen’s height.
Even though we don’t know the size, we know there will always be percentages. The Alignment grid gives a visual way to do that. We start by setting up how many rows and how many columns we want to split the game into.
var gridConfig = { 'scene': this, 'cols': 5, 'rows': 5 }
Now let’s set up the class that uses this config, the Alignment Grid. Since this is a simple object and we will be passing the scene, we don’t need to extend it with anything.
class AlignGrid { constructor(config) { } }
The only required parameter is the scene, everything else is optional. We will verify if the scene is present and if not, we will log an error and return. If the optional parameters are missing we will set defaults. Also, we will promote some of the config object’s properties to class level variables. this.scene = config.scene for example.
class AlignGrid { constructor(config) { if (!config.scene) { console.log("missing scene!"); return; } if (!config.rows) { config.rows = 3; } if (!config.cols) { config.cols = 3; } if (!config.width) { config.width = game.config.width; } if (!config.height) { config.height = game.config.height; } this.h = config.height; this.w = config.width; this.rows = config.rows; this.cols = config.cols; this.scene = config.scene; }
Doing the Math!
Now here is where the magic happens. We will take the height and width passed to the AlignmentGrid class, by default the height and width of the game, and divide it by the numbers of rows and columns and put it in a cell width variable and cell height variable.
//cw cell width is the scene width divided by the number of columns this.cw = this.w / this.cols; //ch cell height is the scene height divided the number of rows this.ch = this.h / this.rows;
Seeing the grid!
Now we have the numbers we need to start placing by percentage. To make it easier for scaling games in Phaser 3 it is better to be able to see the grid. Let’s make a function called show() with a single parameter to alpha the lines, so we can turn the lines down in case we need to see the objects we are positioning better.
//mostly for planning and debugging this will //create a visual representation of the grid show(a = 1) { this.graphics = this.scene.add.graphics(); this.graphics.lineStyle(4, 0xff0000, a); // // //this.graphics.beginPath(); for (var i = 0; i < this.w; i += this.cw) { this.graphics.moveTo(i, 0); this.graphics.lineTo(i, this.h); } for (var i = 0; i < this.h; i += this.ch) { this.graphics.moveTo(0, i); this.graphics.lineTo(this.w, i); } this.graphics.strokePath(); }
Let’s see the grid in action now.
In your create function of sceneMain place this code
var gridConfig = { 'scene': this, 'cols': 5, 'rows': 5 } this.aGrid = new AlignGrid(gridConfig); this.aGrid.showNumbers();
Here is the result.
Placing Object on the grid
Now that we have a grid we are back on familiar ground as programmers because we know how to place objects at x and y coordinates. Let’s set up a function to do that.
//place an object in relation to the grid placeAt(xx, yy, obj) { //calculate the center of the cell //by adding half of the height and width //to the x and y of the coordinates var x2 = this.cw * xx + this.cw / 2; var y2 = this.ch * yy + this.ch / 2; obj.x = x2; obj.y = y2; }
Set up a test object to place on the grid
This part should be fairly well known to you, we will simply load an image and make a sprite.
preload() { this.load.image("face", "images/face.png"); } create() { var gridConfig = { 'scene': this, 'cols': 5, 'rows': 5 } this.aGrid = new AlignGrid(gridConfig); this.aGrid.show(); // // this.face = this.add.image(0, 0, "face"); }
Now place it on the grid by using this line of code:
this.aGrid.placeAt(2,2,this.face);
This will place it 2 squares over and 2 squares down, counting from 0. So this will place it in the middle of the screen
Scaling to the grid
Notice though that the face doesn’t fit in the square. Since we are dividing the width of the game by 5 to make the grid we can resize the face by scaling it related to the game’s width too.
//scale the face this.face.displayWidth = game.config.width / 5; this.face.scaleY = this.face.scaleX;
for more on scaling in Phaser 3 please see this post.
Making it a little easier
For the purpose of scaling games in Phaser 3, this is enough, but it was slowing me down constantly counting the rows and columns of the grids. I am usually using an 11 x 11 grid, and my productivity started to slide. So I came up with a numbering system. This function will call the show function and places numbers in each cell.
showNumbers(a = 1) { this.show(a); var n = 0; for (var i = 0; i < this.rows; i++) { for (var j = 0; j < this.cols; j++) { var numText = this.scene.add.text(0, 0, n, { color: 'red' }); numText.setOrigin(0.5, 0.5); this.placeAt(j, i, numText); n++; } } }
I then made a new function to place by index rather than x and y. It simply converts the index to the grid’s x and y and calls the placeAt function.
placeAtIndex(index, obj) { var yy = Math.floor(index / this.cols); var xx = index - (yy * this.cols); this.placeAt(xx, yy, obj); }
So now replace the placeAt(2,2,this.face) with this code:
this.aGrid.placeAtIndex(18, this.face);
and replace the grid.show() with:
this.aGrid.showNumbers();
Here is the result:
Testing the scaling
In your browser’s tools, resize the screen for various devices. On chrome, this is done by pressing f-12 and then clicking the mobile icon.
Always refresh the page after changing devices. There are a few occasions where I’ve had to tweak placements or sizes for certain devices, but I’ve been using this method in my professional work for almost 2 years now, and I’ve found it has done 99% of the work for me. Although that was Phaser CE, because we are using mathematics this technique will work with any framework. I hope it helps you with scaling games in Phaser 3. Happy coding!!!
Nice post, it help me a lot, thank you!!!