
In this tutorial, you will learn how to create a simple game using tilemaps in Phaser.io. The goal of the game is to click on flowers that appear on the screen as quickly as possible. As time progresses, more flowers will appear, making the challenge increasingly difficult. The game ends when the player can no longer keep up with the growing number of flowers, adding an element of urgency and fast decision-making. Throughout this tutorial, we will explore the use of tilemaps, an essential feature in game development that allows for efficient and structured level design.
Tilemaps are a powerful tool for creating 2D game worlds by arranging small, square-shaped graphics known as tiles in a grid-based layout. These tiles can represent a variety of elements such as terrain, objects, or interactive components, making them an essential part of game environments. In this tutorial, tilemaps will be used to define where flowers appear in the game and to manage the logic behind clicking on them. Before we dive into the coding process, it is important to understand how Tiled, a widely used map editor, can help in designing and structuring tilemaps.
Tiled is a free, open-source tool designed for creating tile-based maps that can be seamlessly integrated into game engines like Phaser. With its user-friendly interface, it allows developers to place tiles on a grid and export these maps into various formats suitable for game development. To begin working with Tiled, you need to install it by downloading the latest version from the official website and following the installation process for your respective operating system, whether it be Windows, Mac, or Linux. Once installed, you can open Tiled and start familiarizing yourself with its interface and features, which will be crucial for designing the game’s layout. By combining the capabilities of Phaser, tilemaps, and Tiled, you will gain a deeper understanding of how to efficiently structure game levels and implement interactive mechanics.
Phaser.io is a game engine that allows you to create games directly in your web browser (like Chrome or Firefox). This means you don’t even need to download any complex software. All you do is write simple code, and voila, your game works in the browser.
– Simple and accessible: It’s perfect for beginners. Even if you’ve never coded before, you can follow this tutorial and create your own game.
– Compatibility: Phaser works on almost every browser, so your friends can play your game too!
– Easy experimentation: You can easily modify your code and see the changes right away.
Before we start coding, you will need a few simple tools:
– A web browser (Chrome, Firefox, Edge, etc.).
– A text editor to write your code (we recommend Visual Studio Code or Notepad if you prefer something simple).
NOTE : If you don’t want to use Visual Studio Code, you can use Notepad, which is already installed on your computer.
HTML is the language used to create web pages. It’s like the skeleton of a page. With HTML, you tell the browser what to display. It is composed of tags defined by the element name surrounded by chevrons.
The HTML document itself begins with `<html>` and ends with `</html>`. The visible part of the HTML document is between `<body>` and `</body>`.
CSS makes web pages look nice. It’s like choosing the colors and styles for your page.
JavaScript makes web pages interactive. It’s the brain behind everything that moves and does actions. Phaser.io uses JavaScript to create video games.
Phaser is actually a JavaScript library. This means it gives you pre-built tools to create games. You don’t have to code everything from scratch!
“`html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″ />
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″ />
<title>My First Phaser Game</title>
<script src=”https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.js”></script>
</head>
<body>
<script>
// This is where our Phaser game starts
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
scene: {
preload: preload,
create: create,
update: update,
},
};
const game = new Phaser.Game(config);
function preload() {
// Load assets here
}
function create() {
// Initialize the game here
}
function update() {
// Game logic goes here
}
</script>
</body>
</html>
“`
– `<!DOCTYPE html>` tells the browser that this is a web page.
– The line `<script src=”https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.js”></script>` loads Phaser.io into your page. This line indicates that we want to load a javascript code that can be found at the address indicated in src.
The config object is like a recipe. It sets the size of your game (800 pixels wide and 600 pixels tall) and defines the game stages (`preload`, `create`, `update`). Several other attributes can be used to define other specific parameters, such as :
– type: Phaser will automatically decide whether to use WebGL or Canvas rendering.
– scale: Ensures the game fits within the browser window.
The preload() function is where you load the images, sounds, or other resources needed for your game. For example, you might load a character or background image here.
The create() function is used to place these images or other elements into your game. This is where you start bringing your game to life.
The update() function runs continuously while the game is active. This is where you write the logic that makes your characters or objects move.
Once you’ve pasted the code into your HTML file, open that file in your web browser. You should see a blank screen (since we haven’t added anything yet), but this is a good start!
If something doesn’t work as expected, don’t worry! Here’s how to find and fix mistakes:
– On most browsers, press `F12` or right-click on the page and select “Inspect”.
– Go to the Console tab. This is where the browser will display error messages when something goes wrong.
Errors are a normal part of coding. The key is to be patient, use the console to spot them, and fix them step by step.
– Open Tiled and create a new map by clicking `File > New…`.
– Set the tile size to `16×16` pixels and select orthogonal for the map orientation.
– Download a tileset (a PNG file with all your tiles) or use one you already have. For this game we will use the sunnyside tileset by @DanielDiggle which you can get here for free: https://danieldiggle.itch.io/sunnyside
– Click `Map > New Tileset…` and load the tileset image.
– Set the tile size to `16×16`.
– Use the different layers in Tiled to create your game map.
– Place tiles that represent different objects in your game (e.g., ground, flowers, etc.).
– Once you’re happy with your map, save it as a `.json` file that Phaser can load.
For the rest of the tutorial and our game, we’ll use our tilemap available in the project’s assets files.
Now that we see how to use Tiled to create a tilemap, let’s build our game.
We’ll begin by setting up a basic HTML file that loads Phaser and our game code.
“`html
<!DOCTYPE html>
<html>
<head>
<script src=”https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser-arcade-physics.min.js”></script>
<script src=”./index.js”></script>
<style>
body, html {
background: black;
margin: 0;
padding: 0;
overflow: hidden;
width: 100%;
height: 100%;
}
canvas {
display: block;
}
</style>
</head>
<body>
</body>
</html>
“`
This sets up our HTML page and loads Phaser and the `index.js` script where we’ll write our game logic.
We’ll now build the logic for the game.
Defining Constants
Add the following constants to your `index.js` file:
“`javascript
const GAME_WIDTH = 800;
const GAME_HEIGHT = 600;
const SEED_TILE_ID = 1011;
const SEED_SPRITE_ID = 823;
const FLOWER_SPRITE_ID = 1012;
const INITIAL_DELAY = 1000;
const MIN_DELAY = 200;
“`
– GAME_WIDTH and GAME_HEIGHT: These define the size of the game screen.
– SEED_TILE_ID: This is the ID of the tile where flowers can grow.
– SEED_SPRITE_ID and FLOWER_SPRITE_ID: These represent the tile IDs for seeds and flowers.
– INITIAL_DELAY: The delay between flower appearances, which decreases over time.
– MIN_DELAY: The minimum time between flower appearances.
Setting Up the Game Scene
Next, we’ll create the main game scene. Add the following code to your `index.js` file:
“`javascript
class MainScene extends Phaser.Scene {
constructor() {
super({ key: ‘MainScene’ });
}
preload() {
// Loading the tileset and map
this.load.spritesheet(“tileset”, “./assets/tileset.png”, { frameWidth: 16, frameHeight: 16 });
this.load.tilemapTiledJSON(“map”, “./assets/map.json”);
}
create() {
this.gameStop = false;
this.score = 0;
this.centerX = GAME_WIDTH / 2;
this.centerY = GAME_HEIGHT / 2;
// Timer start date
this.startTime = Date.now();
// Add the tilemap
const map = this.add.tilemap(“map”);
// Add the tileset
const tileset = map.addTilesetImage(“tileset”, “tileset”);
// Add layers from the tilemap
const propsLayer = map.createLayer(“Sea”, tileset, 0, 0);
const groundLayer = map.createLayer(“Ground”, tileset, 0, 0);
const seedLayer = map.createLayer(“Seed”, tileset, 0, 0);
// Resize layers to fit the game screen
propsLayer.displayWidth = GAME_WIDTH;
groundLayer.displayWidth = GAME_WIDTH;
seedLayer.displayWidth = GAME_WIDTH;
propsLayer.displayHeight = GAME_HEIGHT;
groundLayer.displayHeight = GAME_HEIGHT;
seedLayer.displayHeight = GAME_HEIGHT;
// Calculate tile scaling
this.tileScaleX = GAME_WIDTH / map.widthInPixels;
this.tilescaleY = GAME_HEIGHT / map.heightInPixels;
// Display score and duration
this.scoreText = this.add.text(16, 16, ‘Score: 0’, { fontFamily: ‘Arial’, fontSize: 24, color: ‘white’ });
this.durationText = this.add.text(16, 48, ‘Duration: 0’, { fontFamily: ‘Arial’, fontSize: 24, color: ‘white’ });
// Find seed tiles from the seed layer
this.seedTiles = seedLayer.filterTiles(tile => tile.index === SEED_TILE_ID);
const maxFlowers = this.seedTiles.length;
// Create a flower group
this.flowers = this.add.group({ maxSize: maxFlowers });
// Create flower sprites at each seed tile
this.seedTiles.forEach(tile => {
this.flowers.create(tile.getCenterX(), tile.getCenterY(), ’tileset’, SEED_SPRITE_ID).setScale(this.tileScaleX, this.tileScaleY);
});
// Detect player clicks on flowers
this.input.on(‘pointerdown’, (pointer) => {
const flower = this.flowers.getChildren().find(f => f.getBounds().contains(pointer.x, pointer.y));
this.clickFlower(flower);
});
// Add flowers over time with decreasing delay
this.tick = this.time.addEvent({ delay: INITIAL_DELAY, loop: true, callback: this.addFlower, callbackScope: this });
}
update() {
if (this.gameStop) return;
// Update duration
this.durationText.setText(‘Duration: ‘ + Math.floor((Date.now() – this.startTime) / 1000) + ‘s’);
}
}
“`
Explanation:
– preload(): Loads the tileset and the tilemap JSON file.
– create(): Adds the tilemap, resizes the layers to fit the game screen, and initializes the flowers that the player will click on. It also sets up the score display and starts the game timer.
– update(): Updates the game duration indicator as time passes.
Clicking on Flowers
Now, let’s handle what happens when the player clicks on a flower. Add the following function to the `MainScene` class:
“`javascript
clickFlower(flower) {
if (!flower) return;
// If clicked on a flower
if (flower.frame.name === FLOWER_SPRITE_ID) {
// Turn it back into a seed and increase score
flower.setFrame(SEED_SPRITE_ID);
this.score += 10;
} else if (flower.frame.name === SEED_SPRITE_ID) {
// Clicking on a seed decreases score
this.score -= 5;
}
// Update score text
this.scoreText.setText(‘Score: ‘ + this.score);
}
“`
Explanation:
– clickFlower(): If the player clicks on a flower, the score increases, and the flower is turned back into a seed. Clicking on a seed reduces the score.
We want flowers to appear gradually over time, increasing the difficulty as the game progresses. Add the following function to the `MainScene` class:
“`javascript
addFlower() {
const seeds = this.flowers.getChildren().filter(f => f.frame.name === SEED_SPRITE_ID);
if (seeds.length === 0) {
this.gameOver();
return;
}
// Randomly select a seed and turn it into a flower
const randomIndex = Phaser.Math.Between(0, seeds.length – 1);
seeds[randomIndex].setFrame(FLOWER_SPRITE_ID);
// Gradually decrease the time between flower appearances
this.tick.delay = Math.max(this.tick.delay – this.tick.delay * 0.01, MIN_DELAY);
}
“`
Explanation:
– addFlower(): Randomly selects a seed to turn into a flower. The time between flower appearances decreases as the game progresses, making it harder.
Finally, we’ll handle what happens when the game ends. Add the following functions to the `MainScene` class:
“`javascript
gameOver() {
if (this.gameStop) return;
this.gameStop = true;
this.time.removeEvent(this.tick);
// Display the Game Over message
this.add.text(this.centerX, this.centerY – 30, ‘Game Over’, { fontSize: ’64px’, color: ‘white’, fontFamily: ‘Arial’ }).setOrigin(0.5);
this.add.text(this.centerX, this.centerY + 30, ‘Press any key to restart’, { fontSize: ’32px’, fontFamily: ‘Arial’ }).setOrigin(0.5);
// Restart the game on key press
this.input.keyboard.once(‘keydown’, this.restartGame, this);
}
restartGame() {
this.scene.restart();
}
“`
Explanation:
– gameOver(): Stops the game and shows the “Game Over” message. The player can press any key to restart the game.
– restartGame(): Restarts the game when called.
“`javascript
const GAME_WIDTH = 800;
const GAME_HEIGHT = 600;
const SEED_TILE_ID = 1011;
const SEED_SPRITE_ID = 823;
const FLOWER_SPRITE_ID = 1012;
const INITIAL_DELAY = 1000;
const MIN_DELAY = 200;
class MainScene extends Phaser.Scene {
constructor() {
super({ key: ‘MainScene’ });
}
preload() {
// Loading tileset
this.load.spritesheet(“tileset”, “./assets/tileset.png”, {
frameWidth: 16,
frameHeight: 16
});
// Loading map
this.load.tilemapTiledJSON(“map”, “./assets/map.json”);
}
create() {
this.gameStop = false;
this.score = 0;
this.centerX = GAME_WIDTH / 2;
this.centerY = GAME_HEIGHT / 2;
// Timer start date
this.startTime = Date.now();
// Add the tilemap
const map = this.add.tilemap(“map”);
// Add the tileset
const tileset = map.addTilesetImage(“tileset”, “tileset”);
// Add the layers
const propsLayer = map.createLayer(“Sea”, tileset, 0, 0);
const groundLayer = map.createLayer(“Ground”, tileset, 0, 0);
const sandLayer = map.createLayer(“Sand”, tileset, 0, 0);
const seedLayer = map.createLayer(“Seed”, tileset, 0, 0);
const decorationLayer = map.createLayer(“Decoration”, tileset, 0, 0);
const plantDecorationLayer = map.createLayer(“Plant decoration”, tileset, 0, 0);
const islandLayer = map.createLayer(“Island”, tileset, 0, 0);
// Resize the layers to fit the screen
propsLayer.displayWidth = GAME_WIDTH;
groundLayer.displayWidth = GAME_WIDTH;
sandLayer.displayWidth = GAME_WIDTH;
seedLayer.displayWidth = GAME_WIDTH;
decorationLayer.displayWidth = GAME_WIDTH;
plantDecorationLayer.displayWidth = GAME_WIDTH;
islandLayer.displayWidth = GAME_WIDTH;
propsLayer.displayHeight = GAME_HEIGHT;
groundLayer.displayHeight = GAME_HEIGHT;
sandLayer.displayHeight = GAME_HEIGHT;
seedLayer.displayHeight = GAME_HEIGHT;
decorationLayer.displayHeight = GAME_HEIGHT;
plantDecorationLayer.displayHeight = GAME_HEIGHT;
islandLayer.displayHeight = GAME_HEIGHT;
// Compute the scale of the tiles
this.tileScaleX = GAME_WIDTH / map.widthInPixels;
this.tilescaleY = GAME_HEIGHT / map.heightInPixels;
// Display the score
this.scoreText = this.add.text(16, 16, ‘Score: 0’)
.setFontFamily(‘Arial’)
.setFontSize(24)
.setColor(‘white’)
.setDepth(10); // Prevent the score to be hidden by the obstacles
// Display the duration
this.durationText = this.add.text(16, 48, ‘Duration: 0’)
.setFontFamily(‘Arial’)
// Get the seed tiles
this.seedTiles = seedLayer.filterTiles(tile => tile.index === SEED_TILE_ID);
const maxFlowers = this.seedTiles.length;
// Create flower group
this.flowers = this.add.group({
maxSize: maxFlowers,
});
// Create flowers on seed tiles
this.seedTiles.forEach(tile => {
// Create a sprite for each seed tile
this.flowers.create(tile.getCenterX(), tile.getCenterY(), ’tileset’, SEED_SPRITE_ID).setScale(this.tileScaleX, this.tileScaleY);
});
// Detect click on flowers
this.input.on(‘pointerdown’, (pointer) => {
// Get the flower clicked
const flower = this.flowers.getChildren().find(flower => flower.getBounds().contains(pointer.x, pointer.y));
this.clickFlower(flower);
});
// Add flowers over time with decreasing delay
this.tick = this.time.addEvent({
delay: INITIAL_DELAY,
loop: true,
callback: this.addFlower,
callbackScope: this
});
}
update() {
if (this.gameStop) return;
// Update the duration
this.durationText.setText(‘Duration: ‘ + Math.floor((Date.now() – this.startTime) / 1000 ) + ‘s’);
}
clickFlower(flower) {
if(!flower) return;
// If this is a flower
if(flower.frame.name === FLOWER_SPRITE_ID) {
// Set the sprite to seed
flower.setFrame(SEED_SPRITE_ID);
this.score += 10; // add 10 to the score
} else if(flower.frame.name === SEED_SPRITE_ID) {
// else remove 5 to the score
this.score -= 5;
}
// update the score
this.scoreText.setText(‘Score: ‘ + this.score);
}
addFlower() {
// Get all the seed sprites
const seed = this.flowers.getChildren().filter(flower => flower.frame.name === SEED_SPRITE_ID);
// If there is no seed left, game over
if(seed.length === 0) {
this.gameOver();
return;
}
// Get a random index
const randomIndex = Phaser.Math.Between(0, seed.length – 1);
// Replace the sprite by another
seed[randomIndex].setFrame(FLOWER_SPRITE_ID);
// Decrease the delay
this.tick.delay = Math.max(this.tick.delay – this.tick.delay*0.01, MIN_DELAY);
}
gameOver() {
if(this.gameStop) return;
this.gameStop = true;
this.time.removeEvent(this.tick);
// Listen for a key press to restart the game
this.input.keyboard.once(‘keydown’, this.restartGame, this);
// Display the game over message
this.add.text(this.centerX, this.centerY – 30, ‘Game Over’)
.setFontSize(64)
.setFontFamily(‘Arial’)
.setOrigin(0.5, 0.5)
.setFontStyle(‘bold’)
.setColor(‘white’);
this.add.text(this.centerX, this.centerY + 30, ‘Press any key to restart’)
.setFontSize(32)
.setFontFamily(‘Arial’)
.setOrigin(0.5, 0.5)
}
restartGame() {
this.scene.restart();
}
}
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH,
height: GAME_HEIGHT,
scene: MainScene,
physics: {
default: ‘arcade’,
arcade: {
debug: false
}
},
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
pixelArt: true,
};
const game = new Phaser.Game(config);
“`
The gameplay can be customised through different elements:
For a more detailed introduction to Tiled, you can explore this [Tiled tutorial](https://doc.mapeditor.org/en/stable/manual/introduction/).
Here are a few ideas to improve and expand the game:
– Add more tile layers: You can create more complex environments with multiple tile layers (e.g., obstacles, decorations).
– New challenges: Introduce obstacles that the player has to avoid clicking on, such as thorny plants that decrease the score.
– Difficulty levels: Make the game more challenging by adding multiple flower types that give different scores or appear at different speeds.