Building a mini-game with Phaser.io

Kateryna Dolhusheva
JYSK Tech
Published in
7 min readJan 15, 2024

--

Phaser.io is a free and open-source JavaScript framework specifically designed for building HTML5 games. It provides a robust set of tools and features that make the game development process much easier and faster, even for beginners.

Framework has a well-documented API and a large community of developers who are always willing to help. This makes it easy to learn and use, even if you are not en experienced programmer.

Phaser can be used to create a wide variety of games, from simple 2D platformers to complex RPGs. It supports a wide range of features, including physics, animation, audio, and networking.

Games developed with Phaser can be played on any device with a web browser, including desktops, laptops, tablets, and smartphones. It is written in highly optimized JavaScript code, which means that your games will run smoothly even on older devices.

In short, Phaser.io is a powerful game development framework that is perfect for anyone who wants to create fun and engaging HTML5 games.

Preparing Local Environment

Since Phaser.io is an HTML5 framework, it doesn’t require any special setup for local environment.

Open preferred code editor and create a simple HTML5 project in it.
Then, add <div> element to serve as the game's container in index.html and include Phaser library using <script> tag. In this example we are going to use Phaser 3.70.0.

After this, create a separate JavaScript file main.js that will contain game config and logic and link it to the HTML.

<body>
<div id="game-holder"></div>

<script src="//cdn.jsdelivr.net/npm/phaser@3.70.0/dist/phaser.min.js"></script>
<script src="js/main.js"></script>
</body>

That’s it! Now you are ready to start game building.

Let’s Create a Runner

Configuration

In main.js file we need to define a game config and initialize Phaser game.

Game config object contains a lot of properties, but for this tutorial we should only set renderer, dimensions, physics and default scene.

The type property in Phaser config is a rendering context that we would like to use in our game. It can be Phaser.AUTO, Phaser.CANVAS, Phaser.HEADLESS or Phaser.WEBGL.

Dimensions of canvas element are set with width and height properties.

The physics property defines physics system that should be used in the game together with its config.

// Phaser game config.
let gameConfig = {
type: Phaser.CANVAS,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
parent: "game-holder",
width: 1600,
height: 1200
},
physics: {
default: "arcade",
arcade: {
gravity: {
x: 0,
y: 0
}
}
},
scene: [PlayGame]
}

After we added a config, we should initialize the Phaser game by adding next line to our main.js:

// Init phaser game
new Phaser.Game(gameConfig);

Scene

The idea for mini-game was to create a runner, where we have a player — an elf or Nisse — that is moving in one direction and a bad guy — Grinch — that tries to reach the player. Also, we need a surface to place the player and the bad guy objects on.

So, the prototype looks like this:

Let’s create a scene for our prototype.

First of all, we need to create a class that extends Phaser.Scene. Usually Phaser Scene object contains three magic functions: preload , create and update.

Then, in preload() function we should load assets — images, spritesheets and background.

export default class PlayGame extends Phaser.Scene {

preload() {
// Load surface image
this.load.image("snow", "img/snow-planet.png");

// Load player image
this.load.image("player", "img/nisse.png");

// Load grinch spritesheet
this.load.spritesheet("grinch", "img/grinch.png", {
frameWidth: 59,
frameHeight: 88
});
}

create() {}

update() {}

}

After that, in create() function we need to define an animation for our sprite, draw a surface, place game objects and define collision between them.

export default class PlayGame extends Phaser.Scene {

preload() {
...
}

create() {
// Grinch walk animation
this.anims.create({
key: "walk",
frames: this.anims.generateFrameNumbers("grinch", {
start: 0,
end: 14,
first: 0
}),
frameRate: 10,
repeat: -1,
});

// Draw a big circle
this.bigCircle = this.add.graphics();
this.bigCircle.lineStyle(gameOptions.bigCircleThickness, gameOptions.color.lineColor, 0);
this.bigCircle.strokeCircle(this.game.config.width / 2, this.game.config.height / 2, gameOptions.bigCircleRadius);

// Add player image
this.player = this.initGameCharacters("player");

// Add grinch sprite
this.grinch = this.initGameCharacters("grinch");
// Play animation
this.grinch.play("walk");
this.grinch.body.onOverlap = true;
this.grinch.setScale(2);

// Collision
this.physics.add.overlap(this.player, this.grinch, this.collider, null, this);
}

update() {}

}

We also need to define how objects move, how they are rotated, how object speed is changed — that should be done in update() function.

Controls

How to make player run? Usually, game developers use keyboard and/or mouse input to allow the player move and interact with game world. But what if we change the strategy and use microphone input for that?

Let’s measure microphone input volume and transform this value into a velocity for our Nisse. The higher input volume — the faster Nisse moves. Also we need to put some limitations for the sound volume, so not every noise can be used to make the Nisse run.

We would like player to use clapping for moving their character. According to google an average handclap can reach up to 60–90 decibels. Depending on how player is clapping, how their palms are located and if their hands are relaxed or tensed the minimum value can be 5.6 dB. Since the volume range is pretty big, let’s use a number that is a bit higher than the minimum: 10dB. This is our minimum microphone signal volume.

For measuring the microphone input, we will use default JS media API.

At first, we need to get the access to the microphone with navigator.getUserMedia() function. The first parameter is an object, which specifies the types of media to request. The second parameter is a success callback function. The last parameter is an error callback.

In success callback we should process the input stream and calculate an average input volume.

export function startListening() {
// Ensure compatibility with different browser APIs.
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

// Check if browser supports accessing the microphone.
if (navigator.getUserMedia) {
// Request access to the microphone audio stream.
navigator.getUserMedia({
audio: true
},
// Handle successful audio stream access.
function (stream) {
// Create an AudioContext for audio processing.
let audioContext = new AudioContext();

// Create nodes for audio analysis and processing.
let analyser = audioContext.createAnalyser();
let microphone = audioContext.createMediaStreamSource(stream);
let javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);

// Configure the analyzer node.
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 1024;

// Connect the audio nodes to form a processing pipeline.
microphone.connect(analyser);
analyser.connect(javascriptNode);
javascriptNode.connect(audioContext.destination);

// Function to process audio data on each processing cycle.
javascriptNode.onaudioprocess = function () {
// Array to hold frequency data from the analyzer.
var array = new Uint8Array(analyser.frequencyBinCount);
analyser.getByteFrequencyData(array);

// Calculate the average frequency value.
var values = 0;
var length = array.length;
for (var i = 0; i < length; i++) {
values += (array[i]);
}

// Set a value based on average frequency (average input volume).
playerOptions.micInputVolume = values / length;
};
},
// Handle errors during microphone access.
function (err) {
console.log("The following error occured: " + err.name);
});
} else {
// Log a message if browser doesn't support getUserMedia.
console.log("getUserMedia not supported");
}
}

After that, in scene update() function we can process playerOptions.micInputVolume value and transform it into a velocity for Nisse.

Grinch’s speed is also should be adjusted depending on the distance between him and player. Luckily, Phaser has in-built function Phaser.Math.Distance.Between(x1, y1, x2, y2) that calculates distance between two points and returns a numerical value. So, if the distance between Nisse and Grinch is more than 300, increase Grinch’s speed.

If collision between Grinch and Nisse is detected, stop listening the microphone and set speed for both to zero.

export default class PlayGame extends Phaser.Scene {

preload() {
...
}

create() {
...
}

update() {
// Check if microphone input volume exceeds a threshold.
if (playerOptions.micInputVolume > gameOptions.minMicInputVolume) {
// Calculate player velocity based on mic input volume.
let velocity = parseFloat((playerOptions.micInputVolume / 100).toFixed(2));
// Chech if calculated velocity lower than max allowed velocity.
if (velocity > gameOptions.maxVelocity) {
velocity = gameOptions.maxVelocity;
}
// Set player velocity.
playerOptions.velocity = velocity;
} else {
// Set a negative velocity if clap isn't detected or if it is too quiet.
playerOptions.velocity = playerOptions.negativeVelocity;
}

// If collision is detected, stop all movement.
if (gameOptions.stopListening) {
playerOptions.velocity = 0;
playerOptions.speed = 0;
grinchOptions.speed = 0;
} else {
// Calculate distance between player and grinch.
let distance = Phaser.Math.Distance.Between(this.grinch.x, this.grinch.y, this.player.x, this.player.y);
// If they're far apart, increase grinch's speed.
if (Math.round(distance) > 300) {
grinchOptions.speed = grinchOptions.speed + grinchOptions.velocity;
}
}

// If player has non-zero velocity, adjust movement speed.
if (playerOptions.velocity !== 0) {
let improvedPlayerSpeed = playerOptions.speed + playerOptions.velocity / 100;
// Check if calculated speed is lower that max allowed player speed.
if (improvedPlayerSpeed > playerOptions.maxPlayerSpeed) {
improvedPlayerSpeed = playerOptions.maxPlayerSpeed;
}
// Ensure player speed is positive.
if (parseFloat(improvedPlayerSpeed.toFixed(2)) > 0) {
playerOptions.speed = improvedPlayerSpeed;
} else {
// If speed becomes zero or negative, set a minimum speed.
playerOptions.velocity = 0;
playerOptions.speed = 0.05;
}
}

// Character Rotation and Positioning.
// Update previous angles for both player and grinch.
this.player.previousAngle = this.player.currentAngle;
this.grinch.previousAngle = this.grinch.currentAngle;

// Calculate new angles based on their speeds.
this.player.currentAngle = Phaser.Math.Angle.WrapDegrees(this.player.currentAngle + playerOptions.speed);
this.grinch.currentAngle = Phaser.Math.Angle.WrapDegrees(this.grinch.currentAngle + grinchOptions.speed);

// Apply rotation and positioning to both sprites.
this.rotateGameCharacter(this.player);
this.rotateGameCharacter(this.grinch);
}

}

Test Your Game

That’s it! To test your game, open the index.html file in your web browser.

Here is a short demo of the game we built in this tutorial. Feel free to check the code repository if interested.

Thanks for reading!

--

--