How to create a complete HTML5 “2048” game with Phaser

Read all posts about "" game

Some days ago I showed you ho to create a jQuery-only working game like “Threes” (or “1024?, or “2048?, or…). Now it’s time to do the same game using HTML5 with Phaser framework.

It’s very easy to create tweens and animations, so here what you are about to create:

Play with WASD keys and try to get to 2048, or even higher!!

Before I show you the complete and fully commented source code, keep in mind I used only a white PNG image for the tile and I am storing game field values into a one-dimensional array.

By studying this script you will learn these 10 principles:

* Load graphic assets
* Create Sprites
* Create texts and assign them a style
* Add and remove Sprites to the stage, both as standalone Sprites or as children of existing sprites
* Create Groups
* Sort and loop through groups
* Handle keyboard input
* Create animations using tweens
* Manage callbacks
* Apply color filters to Sprites

A lot of stuff for such a simple game! Here we go:

<!doctype html>
<html>
	<head>
    		<script src="phaser.min.js"></script>
    		<style>
    			body{margin:0}
    		</style>
    		<script type="text/javascript">
			window.onload = function() {
                    // tile width, in pixels
				var tileSize = 100;
				// creation of a new phaser game, with a proper width and height according to tile size
				var game = new Phaser.Game(tileSize*4,tileSize*4,Phaser.CANVAS,"",{preload:onPreload, create:onCreate});
				// game array, starts with all cells to zero
				var fieldArray = new Array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);
				// this is the group which will contain all tile sprites
				var tileSprites;
				// variables to handle keyboard input
				var upKey;
				var downKey;
				var leftKey;
				var rightKey;
				// colors to tint tiles according to their value
				var colors = {
					2:0xFFFFFF,
					4:0xFFEEEE,
					8:0xFFDDDD,
					16:0xFFCCCC,
					32:0xFFBBBB,
					64:0xFFAAAA,
					128:0xFF9999,
					256:0xFF8888,
					512:0xFF7777,
					1024:0xFF6666,
					2048:0xFF5555,
					4096:0xFF4444,
					8192:0xFF3333,
					16384:0xFF2222,
					32768:0xFF1111,
					65536:0xFF0000
				}
				// at the beginning of the game, the player cannot move
                    var canMove=false;
					
				// THE GAME IS PRELOADING
				function onPreload() {
					// preload the only image we are using in the game
					game.load.image("tile", "tile.png");
				}
				
				// THE GAME HAS BEEN CREATED
				function onCreate() {
					// listeners for WASD keys
					upKey = game.input.keyboard.addKey(Phaser.Keyboard.W);
					upKey.onDown.add(moveUp,this);
    					downKey = game.input.keyboard.addKey(Phaser.Keyboard.S);
    					downKey.onDown.add(moveDown,this);
    					leftKey = game.input.keyboard.addKey(Phaser.Keyboard.A);
    					leftKey.onDown.add(moveLeft,this);
    					rightKey = game.input.keyboard.addKey(Phaser.Keyboard.D);
    					rightKey.onDown.add(moveRight,this);
    					// sprite group declaration
					tileSprites = game.add.group();
    					// at the beginning of the game we add two "2"
					addTwo();
					addTwo();
				}
				
				// A NEW "2" IS ADDED TO THE GAME
				function addTwo(){
					// choosing an empty tile in the field
					do{
						var randomValue = Math.floor(Math.random()*16);
					} while (fieldArray[randomValue]!=0)
					// such empty tile now takes "2" value
					fieldArray[randomValue]=2;
					// creation of a new sprite with "tile" instance, that is "tile.png" we loaded before
					var tile = game.add.sprite(toCol(randomValue)*tileSize,toRow(randomValue)*tileSize,"tile");
					// creation of a custom property "pos" and assigning it the index of the newly added "2"
					tile.pos = randomValue;
					// at the beginning the tile is completely transparent
					tile.alpha=0;
					// creation of a text which will represent the value of the tile
					var text = game.add.text(tileSize/2,tileSize/2,"2",{font:"bold 16px Arial",align:"center"});
                         // setting text anchor in the horizontal and vertical center
					text.anchor.set(0.5);
					// adding the text as a child of tile sprite
					tile.addChild(text);
					// adding tile sprites to the group
					tileSprites.add(tile);
					// creation of a new tween for the tile sprite
					var fadeIn = game.add.tween(tile);
					// the tween will make the sprite completely opaque in 250 milliseconds
					fadeIn.to({alpha:1},250);
					// tween callback
					fadeIn.onComplete.add(function(){
						// updating tile numbers. This is not necessary the 1st time, anyway
						updateNumbers();
						// now I can move
						canMove=true;
					})
					// starting the tween
					fadeIn.start();
				}
				
				// GIVING A NUMBER IN A 1-DIMENSION ARRAY, RETURNS THE ROW
				function toRow(n){
					return Math.floor(n/4);
				}
				
				// GIVING A NUMBER IN A 1-DIMENSION ARRAY, RETURNS THE COLUMN
				function toCol(n){
					return n%4;	
				}
				
				// THIS FUNCTION UPDATES THE NUMBER AND COLOR IN EACH TILE
				function updateNumbers(){
					// look how I loop through all tiles
					tileSprites.forEach(function(item){
						// retrieving the proper value to show
						var value = fieldArray[item.pos];
						// showing the value
						item.getChildAt(0).text=value;
						// tinting the tile
						item.tint=colors[value]
					});	
				}
				
				// MOVING TILES LEFT
				function moveLeft(){
					// Is the player allowed to move?
                         if(canMove){
                         	// the player can move, let's set "canMove" to false to prevent moving again until the move process is done
                              canMove=false;
                              // keeping track if the player moved, i.e. if it's a legal move
                              var moved = false;
                              // look how I can sort a group ordering it by a property
     					tileSprites.sort("x",Phaser.Group.SORT_ASCENDING);
     					// looping through each element in the group
     					tileSprites.forEach(function(item){
     						// getting row and column starting from a one-dimensional array
     						var row = toRow(item.pos);
     						var col = toCol(item.pos);
     						// checking if we aren't already on the leftmost column (the tile can't move)
     						if(col>0){
     							// setting a "remove" flag to false. Sometimes you have to remove tiles, when two merge into one 
     							var remove = false;
     							// looping from column position back to the leftmost column
     							for(i=col-1;i>=0;i--){
     								// if we find a tile which is not empty, our search is about to end...
     								if(fieldArray[row*4+i]!=0){
     									// ...we just have to see if the tile we are landing on has the same value of the tile we are moving
     									if(fieldArray[row*4+i]==fieldArray[row*4+col]){
     										// in this case the current tile will be removed
     										remove = true;
     										i--;                                             
     									}
     									break;
     								}
     							}
     							// if we can actually move...
     							if(col!=i+1){
     								// set moved to true
                                             moved=true;
                                             // moving the tile "item" from row*4+col to row*4+i+1 and (if allowed) remove it
                                             moveTile(item,row*4+col,row*4+i+1,remove);
     							}
     						}
     					});
     					// completing the move
     					endMove(moved);
                         }
				}
				
				// FUNCTION TO COMPLETE THE MOVE AND PLACE ANOTHER "2" IF WE CAN
				function endMove(m){
					// if we move the tile...
					if(m){
						// add another "2"
     					addTwo();
                         }
                         else{
                         	// otherwise just let the player be able to move again
						canMove=true;
					}
				}
				
				// FUNCTION TO MOVE A TILE
				function moveTile(tile,from,to,remove){
					// first, we update the array with new values
                         fieldArray[to]=fieldArray[from];
                         fieldArray[from]=0;
                         tile.pos=to;
                         // then we create a tween
                         var movement = game.add.tween(tile);
                         movement.to({x:tileSize*(toCol(to)),y:tileSize*(toRow(to))},150);
                         if(remove){
                         	// if the tile has to be removed, it means the destination tile must be multiplied by 2
                              fieldArray[to]*=2;
                              // at the end of the tween we must destroy the tile
                              movement.onComplete.add(function(){
                                   tile.destroy();
                              });
                         }
                         // let the tween begin!
                         movement.start();
                    }
                    
                    // MOVING TILES UP - SAME PRINCIPLES AS BEFORE
                    function moveUp(){
                          if(canMove){
                              canMove=false;
                              var moved=false;
     					tileSprites.sort("y",Phaser.Group.SORT_ASCENDING);
     					tileSprites.forEach(function(item){
     						var row = toRow(item.pos);
     						var col = toCol(item.pos);
     						if(row>0){  
                                        var remove=false;
     							for(i=row-1;i>=0;i--){
     								if(fieldArray[i*4+col]!=0){
     									if(fieldArray[i*4+col]==fieldArray[row*4+col]){
     										remove = true;
     										i--;                                             
     									}
                                                  break
     								}
     							}
     							if(row!=i+1){
                                             moved=true;
                                             moveTile(item,row*4+col,(i+1)*4+col,remove);
     							}
     						}
     					});
     					endMove(moved);
                         }
				}
				
				// MOVING TILES RIGHT - SAME PRINCIPLES AS BEFORE
                    function moveRight(){
                          if(canMove){
                              canMove=false;
                              var moved=false;
     					tileSprites.sort("x",Phaser.Group.SORT_DESCENDING);
     					tileSprites.forEach(function(item){
     						var row = toRow(item.pos);
     						var col = toCol(item.pos);
     						if(col<3){
                                        var remove = false;
     							for(i=col+1;i<=3;i++){
     								if(fieldArray[row*4+i]!=0){
                                                  if(fieldArray[row*4+i]==fieldArray[row*4+col]){
     										remove = true;
     										i++;                                             
     									}
     									break
     								}
     							}
     							if(col!=i-1){
                                             moved=true;
     								moveTile(item,row*4+col,row*4+i-1,remove);
     							}
     						}
     					});
     					endMove(moved);
                         }
				}
                    
                    // MOVING TILES DOWN - SAME PRINCIPLES AS BEFORE
                    function moveDown(){
                          if(canMove){
                              canMove=false;
                              var moved=false;
     					tileSprites.sort("y",Phaser.Group.SORT_DESCENDING);
     					tileSprites.forEach(function(item){
     						var row = toRow(item.pos);
     						var col = toCol(item.pos);
     						if(row<3){
                                        var remove = false;
     							for(i=row+1;i<=3;i++){
     								if(fieldArray[i*4+col]!=0){
     									if(fieldArray[i*4+col]==fieldArray[row*4+col]){
     										remove = true;
     										i++;                                             
     									}
                                                  break
     								}
     							}
     							if(row!=i-1){
                                             moved=true;
     								moveTile(item,row*4+col,(i-1)*4+col,remove);
     							}
     						}
     					});
     				     endMove(moved);
                         }
				}
	    		};
		</script>
    </head>
    <body>
    </body>
</html>

As usual, you can download the source code of the game with all required libraries

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

215 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
// Stairs
// 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