Build the HTML5 Game "2048" Using the Latest Version of Phaser 3

Create a project
Download the latest version of Phaser.js from the Phaser.io website (the version used in this article is 3.88.2) and install it. Open the Phaser Launcher,- In the
Projects
tab panel. - Click the
New
button in the upper left corner. - In the
Select a template
list, select 2048 template. - Select path for the project in the
Folder
on the right. - Enter the project name in the
Project Name
on the right, for example 2048. - Finally, click the
Create
button.

Understand the project structure
In the Code Editor
interface of the 2048 project created above, as shown in the figure below, on the left side, you can see the project asset folder assets
and the project code folder src
, as well as the project configuration file project.config
.

In Phaser 3 projects, project.config
, index.html
, and main.js
each play different but crucial roles. Below is a detailed introduction to their roles.
project.config
The project.config
file is usually used to store project configuration information, which can help developers and tools better understand and manage the project. Based on the code snippet you provided, this file uses the INI format and contains multiple parts. Its specific functions are as follows:
- Game configuration: The
[game]
section specifies the basic information of the game, such as thetitle
being the title of the game, and thewidth
andheight
being the width and height of the game window.
[game]
title = "2048"
width = 1280
height = 720
[user]
section stores information related to the user, such as the id
being the user's Unique Device Identifier.
[user]
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[editor]
section records information such as the editor version, Phaser version, creation time, and code font size of the project, which helps developers understand the development environment and history of the project.
[editor]
editor_version = "1.1.0"
phaser_version = "3.88.2"
creation_at = 1742569365604
code_font_size = 16
index.html
index.html
is the entry HTML file of the project, which provides the basic HTML structure and environment for the game to run. Its specific functions are as follows:
- Page settings: Set the character encoding, viewport, and title of the page.
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>2048</title>
<style>
/* Reset CSS: Please avoid making changes here unless you know what you're doing :) */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Other style rules */
</style>
div
element as the container for the game, and the Phaser game will be rendered into this container.
<div id="game-container"></div>
<script src="./phaser.js"></script>
<script type="module" src="./src/main.js"></script>
main.js
main.js
is the main JavaScript file of the project, which is responsible for initializing the Phaser game and configuring the basic parameters of the game. Its specific functions are as follows:
- Import scenes: Import various scenes used in the game, such as
Boot
,Preloader
,Game
, andGameOver
scenes.
import { Boot } from './scenes/Boot.js';
import { Game } from './scenes/Game.js';
import { GameOver } from './scenes/GameOver.js';
import { Preloader } from './scenes/Preloader.js';
const config = {
type: Phaser.AUTO,
width: 1024,
height: 768,
parent: 'game-container',
backgroundColor: '#2d3436',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
scene: [
Boot,
Preloader,
Game,
GameOver
]
};
new Phaser.Game(config);
In summary, project.config
stores the configuration information of the project, index.html
provides the HTML environment for the game to run, main.js
initializes the Phaser game and configures basic parameters. They work together to ensure the normal operation of the game.
Core algorithm
- Game state management
- Initialize the chessboard: Create a two-dimensional array
fieldArray
to represent the game chessboard, where each element stores an object containingtileValue
(the value of the tile),tileSprite
(the sprite object of the tile), andcanUpgrade
(whether it can be upgraded). - Add new tiles: After each move, randomly select an empty cell to add a new tile. The value of the new tile has a 90% chance of being 2 and a 10% chance of being 4.
- Check the end of the game: traverse the entire chessboard. If there are no empty cells and the values of adjacent tiles are not equal, the game is over.
- Move and merge logic
- Moving direction processing: Determine the moving direction (up, down, left, right) based on the user's keyboard input or touch sliding event.
- Move Logic: Traverse each tile on the chessboard and move the tile in that direction to the farthest empty cell or adjacent tile of the same value.
- Merge logic: If adjacent tiles have the same value and can be upgraded, merge them into one tile with twice the original value and update the score.
- Animation processing
- New tile appearance animation: The newly added tile is scaled from 0 to 0.8 using
tweens
animation. - Moving animation: During the movement of tiles, use
tweens
animation to achieve linear movement. - Merge animation: The merged tiles are first enlarged to 1.0, then reduced to 0.8, using
tweens
animation.
Implementation process
- Scene loading and initialization
- Boot scene: Preload the basic resources required for the game, such as background images, and then launch the
Preloader
scene. - Preloader scene: Display the progress bar, load all the resources required for the game, such as tile images, and launch the
Game
scene after loading. - Game scene creation and initialization
- Game Scene: Create game and board backgrounds. Initialize the chessboard, create a tile background and tile sprite, and store them in fieldArray. Set up keyboard and touch input events to listen to user movements. Start the game and add two initial tiles.
- Game logic processing
- Handle input events: Call the
handleMove
method to handle the movement logic based on the user's keyboard input or touch sliding events. - Handle movement logic: traverse each tile on the chessboard, move the tile to the farthest empty cell or adjacent tile with the same value according to the direction of movement, and handle merge logic.
- Move and merge animations: Use
tweens
animations to achieve tile movement and merge effects. - Add new tile: After each move, randomly select an empty cell to add a new tile.
- Check the end of the game: traverse the entire chessboard. If there are no empty cells and the values of adjacent tiles are not equal, the game is over.
- Game over processing
- Display the game over interface: At the end of the game, a semi-transparent mask layer is displayed, displaying the game end text, final score, and restart prompt.
- Restart the game: Reset the score and board, and restart the game scene.
export class Boot extends Phaser.Scene {
constructor() {
super('Boot');
}
preload() {
this.load.setPath('assets');
this.load.image('background', 'background.png');
}
create() {
this.scene.start('Preloader');
}
}
export class Preloader extends Phaser.Scene {
constructor() {
super('Preloader');
}
init() {
// Display Background and Progress Bar
}
preload() {
this.load.setPath('assets');
// Load all tile images
}
create() {
this.scene.start('Game');
}
}
export class Game extends Phaser.Scene {
constructor() {
super({ key: 'Game' });
// Initialize game parameters
}
create() {
// Create backgrounds and chessboards
// Initialize the chessboard and tiles
// Set input events
// Add initial tile
}
}
handleKey(e) {
if (!this.canMove) return;
// Handle movement according to key code
this.handleMove(deltaRow, deltaCol);
}
endSwipe(e) {
if (!this.canMove) return;
// Handle movement according to sliding direction
this.handleMove(deltaRow, deltaCol);
}
handleMove(deltaRow, deltaCol) {
this.canMove = false;
let isMoved = false;
// Traverse the chessboard to handle moves and merges
if (!isMoved) {
this.canMove = true;
}
}
moveTile(tile, row, col, distance, changeNumber) {
this.movingTiles++;
// Create moving animations
// Merge or add new tiles after the animation is complete
}
mergeTile(tile, row, col) {
this.movingTiles++;
// Create merge animation
// Add new tiles after the animation is completed
}
addTile() {
// Find all empty cells
// Randomly select an empty cell
// 90% probability is 2, 10% probability is 4.
// Update tile values and sprite textures
// Create new tile appearance animation
// Check the end of the game after the animation is completed
}
checkGameOver() {
for (let row = 0; row < this.boardSize; row++) {
for (let col = 0; col < this.boardSize; col++) {
// Check if there are empty cells or mergeable tiles
}
}
// Game over, display game over interface
}
gameOver() {
// Create mask layer
// Display game over text, score, and restart prompt
// Listen for touch events, click to restart the game
}
restartGame() {
this.score = 0;
this.scoreText.setText(`Score: 0`);
this.scene.restart();
}
Optimization
Delete excess content
SceneGameOver
is redundant and can be deleted. In addition to deleting the GameOver.js
file, the following content with strikethrough also needs to be deleted in main.js
.
import { Boot } from './scenes/Boot.js';
import { Game } from './scenes/Game.js';
import { GameOver } from './scenes/GameOver.js';
import { Preloader } from './scenes/Preloader.js';
// Find out more information about the Game Config at:
// https://newdocs.phaser.io/docs/3.70.0/Phaser.Types.Core.GameConfig
const config = {
type: Phaser.AUTO,
width: 1024,
height: 768,
parent: 'game-container',
backgroundColor: '#2d3436',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
scene: [
Boot,
Preloader,
Game,
GameOver
]
};
new Phaser.Game(config);
Game over interface depth issue
In thegameOver
function in Game.js
, due to its depth issue, the game over interface is blocked by the game board.

setDepth
.
gameOver() {
const overlay = this.add.rectangle(512, 384, 1024, 768, 0x000000, 0.7).setDepth(5000);
const gameOverText = this.add.text(512, 384, 'GAME OVER', {
fontFamily: 'Arial Black',
fontSize: 64,
color: '#ffffff',
align: 'center'
}).setOrigin(0.5).setDepth(5001);
const scoreText = this.add.text(512, 450, `Score: ${this.score}`, {
fontFamily: 'Arial',
fontSize: 32,
color: '#ffffff',
align: 'center'
}).setOrigin(0.5).setDepth(5001);
const restartText = this.add.text(512, 520, 'Tap to restart', {
fontFamily: 'Arial',
fontSize: 24,
color: '#ffffff',
align: 'center'
}).setOrigin(0.5).setDepth(5001);
overlay.setInteractive();
overlay.on('pointerdown', () => {
this.restartGame();
});
}
The result:
