The basics of responsive HTML5 games

Read all posts about "" game

When you are about to create an HTML5 game, you should know most of your players will come from mobile devices. Most – if not all – sponsors look at an HTML5 game only if it perfectly fits in a wide range of mobile phones and tablets, and most – if not all – of them do not like black letterbox bars when your game size ratio does not match the size ratio of the mobile device it’s working on.

So you have to create the game with a lot of different resolutions and screen ratios in mind, or you can just make your game responsive.

In this post we’ll see how to make a responsive HTML5 game starting from the simplest case: when the whole action of your game can be circumscribed in a squared area. This is the case of most match 3 puzzle games, where the only interactive area is inside the 8×8 jewels square. The rest of the game is not interactive, and normally it shows the score or a battle between you and some kind of minions as you match tiles.

This is the best game type to start thinking about responsive games, and tile based puzzle games like BWBan – get it! It’s free and with no ads – are a nice example too.

In the following examples, we are turning the prototype built in the post “HTML5 swipe controlled Sokoban game made with Phaser step 2: adding keyboard controls and unlimited undos” into a responsive game, adding game name and level information respectively on the top and on the bottom of the game, while the action always take place in the middle of the device.

Do you want some examples? This is the same playable game running on three different screen ratios, you can play both with arrow keys or by swiping.

iPhone 4 screen ratio, portrait
iPhone 6 screen ratio, portrait
iPad Mini screen ratio, landscape

Each example has game name and level information at the very top and the very bottom of the stage, with the playable area in the middle of the stage.

This is the source code to achieve it, with highlighted lines to show what’s new from the original example:

window.onload = function() {
	
	// game definition, 100% of browser window dimension
	var game = new Phaser.Game("100%","100%",Phaser.CANVAS,"",{
		preload:onPreload,
		create:onCreate,
		resize:onResize // <- this will be called each time the game is resized
	});                
	
	// constants with game elements
	var EMPTY = 0;
     var WALL = 1;
     var SPOT = 2;
     var CRATE = 3;
     var PLAYER = 4;
     // according to these values, the crate on the spot = CRATE+SPOT = 5 and the player on the spot = PLAYER+SPOT = 6
	
	// sokoban level, using hardcoded values rather than constants to save time, shame on me :) 
	var level = [[1,1,1,1,1,1,1,1],[1,0,0,1,1,1,1,1],[1,0,0,1,1,1,1,1],[1,0,0,0,0,0,0,1],[1,1,4,2,1,3,0,1],[1,0,0,0,1,0,0,1],[1,0,0,0,1,1,1,1],[1,1,1,1,1,1,1,1]];
	
	// array which will keep track of undos
	var undoArray = [];
	
	// array which will contain all crates
	var crates = [];
	
	// size of a tile, in pixels
     var tileSize = 40;
     
     // the player! Yeah!
     var player;
     
     // is the player moving?
     var playerMoving = false;
     
     // variables used to detect and manage swipes
     var startX;
     var startY;
     var endX;
     var endY;
     
     // texts to display game name and level
     var levelText;
     var titleText;
     
     // Variables used to create groups. The fist group is called fixedGroup, it will contain
     // all non-moveable elements (everything but crates and player).
     // Then we add movingGroup which will contain moveable elements (crates and player)
     var fixedGroup;
     var movingGroup;

     // first function to be called, when the game preloads I am loading the sprite sheet with all game tiles
	function onPreload() {
		game.load.spritesheet("tiles","tiles.png",40,40);
	}

	// function to scale up the game to full screen
	function goFullScreen(){
		// setting a background color
		game.stage.backgroundColor = "#555555";
		game.scale.pageAlignHorizontally = true;
		game.scale.pageAlignVertically = true;
		// using RESIZE scale mode
		game.scale.scaleMode = Phaser.ScaleManager.RESIZE;
		game.scale.setScreenSize(true);
	}

	// function to be called when the game has been created
	function onCreate() {
		// waiting for a key pressed
		game.input.keyboard.addCallbacks(this,onDown);
		// going full screen with the function defined at line 32
		goFullScreen(); 
		// adding the two groups to the game
          fixedGroup = game.add.group();
          movingGroup = game.add.group();
		// drawing the level 
          drawLevel();
		// once the level has been created, we wait for the player to touch or click, then we call
		// beginSwipe function
		game.input.onDown.add(beginSwipe, this);
		// placing a text on the horizontal center and bottom of the stage
		levelText = game.add.text(game.width/2,game.height,"Level 1",{
			font:"bold 24px Arial",
			fill: "#ffffff"
		});
		// changing anchor so I don't have to calculate an offset based on text height
		levelText.anchor.y=1;
		// placing a text on the horizontal center and top of the stage
		titleText = game.add.text(game.width/2,0,"BWBan",{
			font:"bold 24px Arial",
			fill: "#ffffff"
		});        
		// normall, onResize is called each time the browser is resized, anyway I am calling it the first time
		// to place all responsive elements in their right positions.
          onResize();
	}
	
	function onResize(){
		// this function is called each time the browser is resized, and re-positions
		// game elements to keep them in their right position according to game size
		levelText.x = Math.round((game.width-levelText.width)/2);
		levelText.y = game.height;
		titleText.x = Math.round((game.width-titleText.width)/2);
		fixedGroup.x = Math.round((game.width-320)/2);
          fixedGroup.y = Math.round((game.height-320)/2);
		movingGroup.x = Math.round((game.width-320)/2);
          movingGroup.y = Math.round((game.height-320)/2);		
	}
	
	function drawLevel(){  
          // empty crates array. Don't use crates = [] or it could mess with pointers
          crates.length = 0;      
          // variable used for tile creation
          var tile
          // looping trough all level rows
		for(var i=0;i<level.length;i++){
			// creation of 2nd dimension of crates array
               crates[i]= [];
               // looping through all level columns
			for(var j=0;j<level[i].length;j++){
				// by default, there are no crates at current level position, so we set to null its
				// array entry
                    crates[i][j] = null;
                    // what do we have at row j, col i?
				switch(level[i][j]){
                         case PLAYER:
                         case PLAYER+SPOT:
                         	// player creation
                              player = game.add.sprite(40*j,40*i,"tiles");
                              // assigning the player the proper frame
						player.frame = level[i][j];
                              // creation of two custom attributes to store player x and y position
                              player.posX = j;
                              player.posY = i;
                              // adding the player to movingGroup
                              movingGroup.add(player);
                              // since the player is on the floor, I am also creating the floor tile
                              tile = game.add.sprite(40*j,40*i,"tiles");
				          tile.frame = level[i][j]-PLAYER;
				          // floor does not move so I am adding it to fixedGroup
                              fixedGroup.add(tile);
                              break;
                         case CRATE:
                         case CRATE+SPOT:
                         	// crate creation, both as a sprite and as a crates array item
                              crates[i][j] = game.add.sprite(40*j,40*i,"tiles");
                              // assigning the crate the proper frame
                              crates[i][j].frame = level[i][j];
                              // adding the crate to movingGroup
                              movingGroup.add(crates[i][j]);
                              // since the create is on the floor, I am also creating the floor tile
                              tile = game.add.sprite(40*j,40*i,"tiles");
				          tile.frame = level[i][j]-CRATE;
				          // floor does not move so I am adding it to fixedGroup
                              fixedGroup.add(tile);                              
                              break;
                         default:
                         	// creation of a simple tile
                              tile = game.add.sprite(40*j,40*i,"tiles");
				          tile.frame = level[i][j];
                              fixedGroup.add(tile);
                    }
			}
		}
	}
	
	// function to be executed once a key is down
	function onDown(e){
		// if the player is not moving...
		if(!playerMoving){
			switch(e.keyCode){
				// left
				case 37:
					move(-1,0);
					break;
				// up
				case 38:
					move(0,-1);
					break;
				// right
				case 39:
					move(1,0);
					break;
				// down
				case 40:
					move(0,1);
					break;
				// undo
				case 85:
					// if there's something to undo...
					if(undoArray.length>0){
						// then undo! and remove the latest move from undoArray
						var undoLevel = undoArray.pop();
						fixedGroup.destroy();
     					movingGroup.destroy();
     					level = [];
     					level = copyArray(undoLevel);
     					drawLevel();
					}
					break;
			}
		}
	}
	
	// when the player begins to swipe we only save mouse/finger coordinates, remove the touch/click
	// input listener and add a new listener to be fired when the mouse/finger has been released,
	// then we call endSwipe function
	function beginSwipe(){
		startX = game.input.worldX;
		startY = game.input.worldY;
		game.input.onDown.remove(beginSwipe);
     	game.input.onUp.add(endSwipe);
	}
	
	// function to be called when the player releases the mouse/finger
	function endSwipe(){
		// saving mouse/finger coordinates
		endX = game.input.worldX;
		endY = game.input.worldY;
		// determining x and y distance travelled by mouse/finger from the start
		// of the swipe until the end
		var distX = startX-endX;
		var distY = startY-endY;
		// in order to have an horizontal swipe, we need that x distance is at least twice the y distance
		// and the amount of horizontal distance is at least 10 pixels
		if(Math.abs(distX)>Math.abs(distY)*2 && Math.abs(distX)>10){
			// moving left, calling move function with horizontal and vertical tiles to move as arguments
			if(distX>0){
                    move(-1,0);
               }
               // moving right, calling move function with horizontal and vertical tiles to move as arguments
               else{
                    move(1,0);
               }
		}
		// in order to have a vertical swipe, we need that y distance is at least twice the x distance
		// and the amount of vertical distance is at least 10 pixels
		if(Math.abs(distY)>Math.abs(distX)*2 && Math.abs(distY)>10){
			// moving up, calling move function with horizontal and vertical tiles to move as arguments
			if(distY>0){
                    move(0,-1);
               }
               // moving down, calling move function with horizontal and vertical tiles to move as arguments
               else{
                    move(0,1);
               }
		}	
		// stop listening for the player to release finger/mouse, let's start listening for the player to click/touch
		game.input.onDown.add(beginSwipe);
     	game.input.onUp.remove(endSwipe);
	}
     
     // function to move the player
     function move(deltaX,deltaY){
     	// if destination tile is walkable...
          if(isWalkable(player.posX+deltaX,player.posY+deltaY)){
          	// push current situation in the undo array
			undoArray.push(copyArray(level));
               // then move the player and exit the function
			movePlayer(deltaX,deltaY);
			return;
          }
          // if the destination tile is a crate... 
          if(isCrate(player.posX+deltaX,player.posY+deltaY)){
          	// ...if  after the create there's a walkable tils...
               if(isWalkable(player.posX+2*deltaX,player.posY+2*deltaY)){
               	// push current situation in the undo array
				undoArray.push(copyArray(level));
				// move the crate
                    moveCrate(deltaX,deltaY);			  
                    // move the player	
				movePlayer(deltaX,deltaY);
               }
          }
     }
     
     // a tile is walkable when it's an empty tile or a spot tile
     function isWalkable(posX,posY){
		return level[posY][posX] == EMPTY || level[posY][posX] == SPOT;
	}
	
	// a tile is a crate when it's a... guess what? crate, or it's a crate on its spot
	function isCrate(posX,posY){
		return level[posY][posX] == CRATE || level[posY][posX] == CRATE+SPOT;
	}
	
	// function to move the player
	function movePlayer(deltaX,deltaY){
		// now the player is moving
		playerMoving = true;
		// moving with a 1/10s tween
		var playerTween =game.add.tween(player);
		playerTween.to({
			x:player.x+deltaX*tileSize,
			y:player.y + deltaY*tileSize
		}, 100, Phaser.Easing.Linear.None,true);
		// setting a tween callback 
		playerTween.onComplete.add(function(){
			// now the player is not moving anymore
			playerMoving = false;
		}, this);
		// updating player old position in level array   
          level[player.posY][player.posX]-=PLAYER;  
          // updating player custom posX and posY attributes
          player.posX+=deltaX;
          player.posY+=deltaY;
          // updating player new position in level array 
          level[player.posY][player.posX]+=PLAYER;  
		// changing player frame accordingly  
          player.frame = level[player.posY][player.posX];
	}
	
	// function to move the crate
	function moveCrate(deltaX,deltaY){
		// moving with a 1/10s tween
		var crateTween =game.add.tween(crates[player.posY+deltaY][player.posX+deltaX]);
		crateTween.to({
			x:crates[player.posY+deltaY][player.posX+deltaX].x+deltaX*tileSize,
			y:crates[player.posY+deltaY][player.posX+deltaX].y+deltaY*tileSize,
		}, 100, Phaser.Easing.Linear.None,true);
		// updating crates array   
          crates[player.posY+2*deltaY][player.posX+2*deltaX]=crates[player.posY+deltaY][player.posX+deltaX];
          crates[player.posY+deltaY][player.posX+deltaX]=null;
          // updating crate old position in level array  
          level[player.posY+deltaY][player.posX+deltaX]-=CRATE;
          // updating crate new position in level array  
     	level[player.posY+2*deltaY][player.posX+2*deltaX]+=CRATE;
     	// changing crate frame accordingly  
     	crates[player.posY+2*deltaY][player.posX+2*deltaX].frame=level[player.posY+2*deltaY][player.posX+2*deltaX];
	}
	
	// need a recursive function to copy arrays, no need to reinvent the wheel so I got it here
	// http://stackoverflow.com/questions/10941695/copy-an-arbitrary-n-dimensional-array-in-javascript 
	function copyArray(a){
		var newArray = a.slice(0);
    		for(var i = newArray.length; i>0; i--){
			if(newArray[i] instanceof Array){
				newArray[i] = copyArray(newArray[i]);	
			}
		}
		return newArray;
	}
}

This is how you can make basic games responsive, obviously there’s more to know if your games have to scale in more complicated ways, but I will cover all cases in future posts. Meanwhile you can download the complete source code.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers