
In this unit, you’ll learn how to use Phaser with its integrated Matter.js physics engine. We’ll explore advanced collisions via a game inspired by the “suika game”, in which the aim is to merge marbles to create larger ones without overflowing the container. The main objective is to introduce you to Matter.js and its features, including physics-based object manipulation, collisions and interactions in a simple game environment.
Trash Merge is an engaging and educational game designed to illustrate the principles of recycling and waste management through interactive gameplay. The objective is to carefully drop and merge waste materials to create new, more refined objects, ultimately working toward a fully recycled planet. Players must strategically manage space and plan their merges efficiently to prevent overflow and maximize their recycling efforts. The challenge lies in organizing the materials effectively, ensuring that each waste item is combined at the right time to progress toward the ultimate goal.
The game begins with small, everyday waste items, such as eggshells, which serve as the foundation for the merging process. When two identical waste items collide, they merge to form a new, more processed material. As players continue merging, they witness how simple discarded objects can transform into more complex materials, mimicking real-life recycling cycles. The process follows a structured sequence: eggshells break down into rotting fruit, which then merges into crumpled paper balls. These paper materials progress into newspaper crumbles, leading to the creation of electronic waste such as old CDs. As the recycling chain advances, electronic waste turns into glass marbles, which merge into bottle caps, then used coffee pods, and finally crushed plastic bottles. The ultimate achievement is the successful combination of two crushed plastic bottles, which results in the creation of a fully recycled planet, symbolizing the completion of the waste transformation cycle.
Players must carefully manage the available space within the jar, ensuring that merges happen efficiently to avoid overflow. If the jar becomes too crowded with waste and no further merges are possible, the game ends, encouraging players to think critically about waste accumulation and the importance of proper recycling. The game not only provides an entertaining experience but also reinforces essential environmental lessons, helping players understand how small, individual recycling actions contribute to a larger global effort.
Through its intuitive mechanics and progressive challenge, Trash Merge offers an engaging way to explore sustainability concepts while testing players’ strategic thinking. It serves as a powerful reminder that waste is not simply discarded but can be repurposed into something valuable when managed correctly. The game’s structure encourages continuous improvement, motivating players to refine their approach and strive for the highest level of recycling efficiency. By combining entertainment with environmental education, Trash Merge transforms waste management into an interactive and rewarding learning experience, ultimately inspiring players to apply these principles in real life.
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.
Start by creating the `index.html` file, which will load the Phaser library and your game script (`index.js`). Additionally, this setup introduces a new approach for displaying the score counter using HTML, CSS, and JavaScript.
“`html
<!DOCTYPE html>
<html>
<head>
<script src=”https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js”></script>
<script src=”./index.js”></script>
<style>
body, html {
font-family: Arial, sans-serif;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
}
body {
background-image: url(./assets/background.png);
background-repeat: repeat;
}
canvas {
display: block;
}
.score-counter {
position: absolute;
color: black;
top: 20px;
left: 20px;
}
.score {
margin: 0;
}
</style>
</head>
<body>
<div class=”score-counter”>
<h3>Score: <span id=”score”>0</span></h3>
</div>
</body>
</html>
“`
Phaser Integration:
The Phaser library is loaded from a CDN, and the `index.js` file will contain the game logic.
Background Styling:
The game area is styled with CSS, applying a repeating background image from `assets/background.png`. This ensures a seamless and engaging game environment.
New Score Display System:
Unlike previous games where the score was displayed using Phaser’s `this.add.text()`, this game handles score updates using HTML and JavaScript.
Structure:
A new `<div>` element with the class `”score-counter”` is added to the page. Inside this div, an `<h3>` element displays the score label, and a `<span>` with the `id=”score”` is used to dynamically update the player’s score. (see [MDN Documentation – Basic HTML syntax](https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Structuring_content/Basic_HTML_syntax))
– CSS Styling:
– The `.score-counter` class positions the score at the top-left corner (`top: 20px; left: 20px;`).
– The color is set to black for contrast.
– The score `<span>` is easily accessible via JavaScript for real-time updates.
Why This Approach?
– Using HTML and CSS for the score allows better customization and flexibility compared to Phaser’s in-game text rendering.
– This method makes it easier to style, animate, or integrate the score counter with other UI elements.
– The score can be updated dynamically using JavaScript with:
“`javascript
document.getElementById(‘score’).innerText = newScore;
“`
This hybrid approach of using Phaser for gameplay and HTML/CSS/JS for UI elements.
The game will have a single scene where all the logic takes place. We’ll start by creating an empty class for the game scene and setting up Phaser’s configuration.
“`javascript
class MainScene extends Phaser.Scene {
constructor() {
super({ key: ‘MainScene’ });
}
preload() {
// Game assets
}
create() {
// Setup game variables here
}
}
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH, // will be defined next
height: GAME_HEIGHT, // will be defined next
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: MainScene,
transparent: true,
physics: {
default: ‘matter’,
matter: {
gravity: {
x: 0,
y: 1
},
debug: false,
}
},
};
const game = new Phaser.Game(config);
“`
We create the basic structure of a Phaser scene. The `create()` method contains all the logic for initializing game objects, while `preload()` takes care of loading all the resources needed by the game.
Then we’ll configure Phaser to initialize our game, specifying the game’s width and height, and defining matter as the physics engine. We modify the configuration of the following elements:
– physics: We are using the Matter.js physics engine with gravity set to pull objects downwards.
– transparent: Allows the background to be customized via the HTML body.
We’ll now define the game constants, which include the dimensions of the game, ball properties, and physics settings.
“`javascript
const GAME_WIDTH = 1000;
const GAME_HEIGHT = 1000;
// Jar : box dimensions
const BOX_WIDTH = 600;
const BOX_HEIGHT = 700;
const SIDE_WIDTH = 30;
“`
Now we need to define the physics of the wall material for our jar. The physics of a body is defined by a series of parameters.
These properties control how objects behave in the physical world, such as how they collide, bounce, and interact with one another. Understanding these properties is crucial for fine-tuning the physics behavior of objects like walls and balls.
Here’s a breakdown of the physics properties we used for the walls in our jar:
“`javascript
const WALL_PHYSICS = {
density: 1000,
restitution: 0,
friction: 1,
frictionStatic: 1,
isStatic: true,
};
“`
Let’s explain each of these properties:
`density`
A `Number` that defines the body’s density (mass per unit area).
The density property controls the mass of the object, which affects how much force is required to move it. In physics engines like Matter.js, density is directly tied to the object’s ability to resist forces such as gravity or collisions.
– High density: Objects with a higher density are heavier and harder to move, which is why we set a high density for the jar’s walls. This ensures that the walls won’t move when balls collide with them.
– Low density: Objects with lower density are lighter and easier to push or move.
`restitution`
A `Number` that defines the restitution (elasticity) of the body. The value is always positive and is in the range (`0`, `1`).
– A value of 0 means collisions may be perfectly inelastic and no bouncing may occur.
– A value of 0.8 means the body may bounce back with approximately 80% of its kinetic energy.
Note that collision response is based on pairs of bodies, and that restitution values are combined with the following formula:
“`javascript
Math.max(bodyA.restitution, bodyB.restitution)
“`
`friction`
A `Number` that defines the friction of the body. The value is always positive and is in the range (`0`, `1`).
– A value of 0 means that the body may slide indefinitely.
– A value of 1 means the body may come to a stop almost instantly after a force is applied.
The effects of the value may be non-linear. High values may be unstable depending on the body. The engine uses a Coulomb friction model including static and kinetic friction. Note that collision response is based on pairs of bodies, and that friction values are combined with the following formula:
“`javascript
Math.min(bodyA.friction, bodyB.friction)
“`
A `Number` that defines the static friction of the body (in the Coulomb friction model).
A value of `0` means the body will never ‘stick’ when it is nearly stationary and only dynamic friction is used.
The higher the value (e.g. `10`), the more force it will take to initially get the body moving when nearly stationary. This value is multiplied with the friction property to make it easier to change friction and maintain an appropriate amount of static friction.
The isStatic property defines whether an object is affected by forces like gravity or collisions.
A static body can never change position or angle and is completely fixed.
– `isStatic: true`: The object remains fixed in place and is not influenced by gravity, collisions, or other forces. This is essential for the jar’s walls, as we do not want them to move when balls collide with them.
– `isStatic: false`: The object will be dynamic, meaning it can move or be affected by gravity and collisions.
For our jar, `isStatic: true` ensures that the walls remain stable and do not fall or shift regardless of the physics interactions happening inside the jar.
Summary of Physics Properties
– Density: Determines the mass of the object and how resistant it is to forces.
– Restitution: Controls how much the object bounces when it collides.
– Friction: Defines how much resistance the object faces when sliding against other surfaces.
– Static Friction: Controls how much force is needed to make a stationary object start moving.
– isStatic: Determines whether the object is fixed in place or dynamic (affected by physics forces).
By tuning these properties, you can achieve different behaviors for the objects in your game, whether you want them to be solid and immovable like the jar walls, or dynamic and interactive like the balls.
Explanation:
– BALLS: This array contains properties for different levels of balls, with increasing size (radius) and different colors.
– WALL_PHYSICS: Physics settings for the jar walls (static with no bounce).
The jar will be created using polygon shapes. We’ll define the walls of the jar using Matter.js’s `fromVertices` method, which allows us to use custom shapes for physics bodies.
“`javascript
// Create the jar walls from the box dimensions to draw points that will create the shape
// for both physics body and graphics
createWallsBody() {
const innerLeftSide = 0;
const innerRightSide = BOX_WIDTH;
const outerLeftSide = innerLeftSide – SIDE_WIDTH;
const outerRightSide = innerRightSide + SIDE_WIDTH;
const topSide = 0;
const innerBottomSide = topSide + BOX_HEIGHT;
const outerBottomSide = innerBottomSide + SIDE_WIDTH;
return [
{ x: innerLeftSide, y: innerBottomSide },
{ x: innerLeftSide, y: topSide },
{ x: outerLeftSide, y: topSide },
{ x: outerLeftSide, y: outerBottomSide },
{ x: outerRightSide, y: outerBottomSide },
{ x: outerRightSide, y: topSide },
{ x: innerRightSide, y: topSide },
{ x: innerRightSide, y: innerBottomSide },
];
}
“`
Explanation
– This function defines the polygon shape of the jar using an array of x, y coordinates.
– The structure consists of:
– Inner sides (defining the actual jar opening and width).
– Outer sides (providing extra boundary thickness to ensure stability in the physics engine).
– The order of points follows a clockwise pattern to create a closed shape.
Now let’s implement the elements in the scene :
“`javascript
create() {
this.score = 0;
this.balls = []; // Balls in the jar
this.isDropping = false; // If a ball is currently dropping
this.gameStop = false;
this.pointerX = this.centerX;
this.centerX = this.cameras.main.width / 2;
this.centerY = this.cameras.main.height / 2;
this.boxY = this.cameras.main.height – ((BOX_HEIGHT) / 2);
this.leftBox = this.centerX – BOX_WIDTH / 2;
this.rightBox = this.centerX + BOX_WIDTH / 2;
// Create the jar points
const points = this.createWallsBody();
// Create the jar physics body
this.boxBound = this.matter.add.fromVertices(this.centerX, this.boxY, points, WALL_PHYSICS);
// Create the jar graphics
const boxImg = this.add.polygon(this.centerX, this.boxY, points, 0xc78e53);
boxImg.setStrokeStyle(2, 0xb17524);
this.matter.add.gameObject(boxImg, this.boxBound);
// Ground collision detection
this.ground = this.matter.add.rectangle(this.centerX, GAME_HEIGHT, GAME_WIDTH, 20, { isStatic: true, isSensor: true });
// Create flying ball
this.createFlyingBall();
// Get score text element from HTML DOM
this.scoreText = document.getElementById(‘score’);
this.scoreText.innerText = 0;
}
“`
Explanation :
– Custom Physics Body for the Jar: Instead of a simple rectangle, the jar is manually defined using a set of points.
– Physics and Rendering Separation:
– `fromVertices()` creates the physical boundaries of the jar.
– `polygon()` renders a graphical representation to match the physics body.
– Collision System:
– `this.ground` acts as an invisible sensor to detect when balls reach the bottom.
– Score Management:
– Instead of Phaser’s built-in text system, the score is managed using an HTML element (`document.getElementById(‘score’)`), allowing for better CSS styling and UI integration.
The balls in our game represent different types of waste items, each with a unique size and sprite. To introduce these objects into the game, we first need to preload the assets. This is done in the `preload()` function, where all the necessary images are loaded before the game starts :
“`javascript
preload() {
this.load.setBaseURL(‘./assets/’); // Load assets folder
this.load.image(‘cap’, ‘wastes/cap.png’);
this.load.image(‘coffee’, ‘wastes/coffee.png’);
this.load.image(‘dvd’, ‘wastes/dvd.png’);
this.load.image(‘eggshell’, ‘wastes/eggshell.png’);
this.load.image(‘glass’, ‘wastes/glass.png’);
this.load.image(‘newspaper’, ‘wastes/newspaper.png’);
this.load.image(‘paper_ball’, ‘wastes/paper_ball.png’);
this.load.image(‘plastic_bottle’, ‘wastes/plastic_bottle.png’);
this.load.image(‘rotten_fruit’, ‘wastes/rotten_fruit.png’);
this.load.image(‘basket_ball’, ‘wastes/basket_ball.png’);
}
“`
The `preload()` function ensures that all game assets (images, sounds, animations) are fully loaded before the game starts. This prevents delays or missing textures when objects appear in the game.
We first set the base folder for assets using `this.load.setBaseURL(‘./assets/’)`, allowing us to reference assets more easily.
We then load each image corresponding to different types of waste items, which will later be used as balls in the game.
By loading all images in advance, we avoid performance issues, ensuring a smooth gameplay experience.
Now that we have loaded the assets, we need to define the properties of each ball. Each ball level has:
– A unique index (determining its difficulty or size).
– A radius (determining how big it appears in the game).
– A sprite reference (pointing to the corresponding preloaded image).
“`javascript
// Other constant variable like GAME_WIDTH, GAME_HEIGHT …
// […]
const BALLS = [
{ level: 1, radius: 20, sprite: ‘cap’ },
{ level: 2, radius: 25, sprite: ‘paper_ball’ },
{ level: 3, radius: 40, sprite: ‘newspaper’ },
{ level: 4, radius: 50, sprite: ‘eggshell’ },
{ level: 5, radius: 60, sprite: ‘plastic_bottle’ },
{ level: 6, radius: 70, sprite: ‘coffee’ },
{ level: 7, radius: 80, sprite: ‘glass’ },
{ level: 8, radius: 100, sprite: ‘rotten_fruit’ },
{ level: 9, radius: 130, sprite: ‘dvd’ },
{ level: 10, radius: 180, sprite: ‘basket_ball’ },
];
“`
Each time a new ball is created, the game will randomly assign a sprite and size based on this list. Since some objects (e.g., a basketball) are significantly larger than others (e.g., a bottle cap), this variation makes the gameplay more dynamic.
We then want to be able to generate a ball with a random level to be deposited in the jar, but we want to avoid generating levels that are too high, otherwise it would be too easy! So we set a limit to the maximum level that can be chosen randomly (constant that we will use in the bullet generation method right after) :
“`javascript
const BALL_LEVEL_GENERATION_LIMIT = 5;
“`
The FlyingBall class represents the ball controlled by the player. This class holds the ball’s graphics and properties, and it has methods for positioning and destruction.
“`javascript
class FlyingBall {
constructor(scene, x, y, ballType) {
this.scene = scene;
this.ballType = ballType;
this.radius = ballType.radius;
// Create the ball graphics
this.graphics = this.scene.add.image(x, y, ballType.sprite);
this.graphics.displayWidth = this.radius * 2;
this.graphics.displayHeight = this.radius * 2;
}
// x getter : return this.graphics.x (alias of this.graphics.x)
get x() {
return this.graphics.x;
}
// y getter : return this.graphics.x (alias of this.graphics.y)
get y() {
return this.graphics.y;
}
setPosition(x, y) {
this.graphics.setPosition(x, y);
}
destroy() {
this.graphics.destroy();
}
}
“`
Explanation:
– FlyingBall: Represents the ball that flies across the screen. It contains methods to update its position and destroy it when no longer needed.
– setPosition(): Sets the position of the ball, used when the player moves the mouse.
Then we create the ball generating system:
“`javascript
createFlyingBall() {
// Create a random ball
const ball = randomBall();
this.currentBall = new FlyingBall(this, this.centerX, 30, ball);
this.updateFlyingBallPosition();
}
function randomBall() {
const level = Phaser.Math.Between(1, BALL_LEVEL_GENERATION_LIMIT);
return BALLS[level – 1];
}
“`
Explanation:
– createFlyingBall(): Creates a new ball and places it at the top of the screen, controlled by the player.
– randomBall(): Randomly selects a ball level between 1 and the defined limit using `Phaser.Math.Between()`.
The player can move the flying ball horizontally by moving the mouse. The ball’s position is updated within the bounds of the jar.
“`javascript
pointermove(pointer) {
if (this.gameStop) return;
// Update the flying ball position
this.pointerX = pointer.x;
this.updateFlyingBallPosition();
}
updateFlyingBallPosition() {
if (!this.currentBall) return;
this.currentBall.setPosition(
Phaser.Math.Clamp(this.pointerX, this.leftBox + this.currentBall.radius, this.rightBox – this.currentBall.radius),
this.currentBall.y
);
}
“`
Explanation:
– pointermove(): Updates the x-position of the flying ball based on the mouse’s position.
– Phaser.Math.Clamp(): Ensures that the ball remains within the bounds of the jar, preventing it from moving outside the edges.
Don’t forget to add this line in the `create()` method to link the event to `pointermove()` :
“`javascript
this.input.on(‘pointermove’, this.pointermove, this);
“`
The player drops the flying ball into the jar by clicking the mouse. Once dropped, a new flying ball is generated after a short delay. When the player drops a ball, we actually create a ball object identical to the location of the ball. Identical except for the addition of the physical body, which allows us to subject the marble to gravity and to collisions between other marbles. To do this, we first define a new global constant representing the physical body of the ball with its properties :
“`javascript
const BALL_PHYSICS = {
restitution: 0,
friction: 0,
density: 1000,
};
“`
We’ve now added mouse click detection and ball deposit :
“`javascript
pointerdown() {
if (this.gameStop || this.isDropping || !this.currentBall) return;
this.isDropping = true;
const [x, y] = [this.currentBall.x, this.currentBall.y];
// Drop the ball
this.dropBall(x, y, this.currentBall.ballType);
// Delete the current ball
this.currentBall.destroy();
// Create the new flying ball
this.time.delayedCall(500, () => {
this.createFlyingBall();
this.isDropping = false;
});
}
// Drop the ball in the jar
dropBall(x, y, type) {
const radius = type.radius; // Get level radius
// Create ball graphics
const ballGraphics = this.matter.add.image(x, y, type.sprite, null, BALL_PHYSICS);
ballGraphics.displayWidth = radius * 2;
ballGraphics.displayHeight = radius * 2;
ballGraphics.ballType = type
ballGraphics.setCircle(radius);
// Push the ball in the array
this.balls.push(ballGraphics);
}
“`
Explanation:
– pointerdown(): Handles the event when the player clicks the mouse, dropping the ball into the jar.
– dropBall(): Creates the ball’s physics body and adds it to the jar. The ball is added to an array to track all active balls in the game.
Don’t forget to add this line in the `create()` method to link the event to `pointerdown()` :
“`javascript
this.input.on(‘pointerdown’, this.pointerdown, this);
“`
We’ll use Matter.js’s `collisionstart` event to detect when two balls collide.
Add this line in the `create()` method :
“`javascript
this.matter.world.on(‘collisionstart’, this.handleCollision, this);
“`
Now let’s implement our collision detection method :
“`javascript
handleCollision(event) {
for (let i = 0, len = event.pairs.length; i < len; i++) {
const { bodyA, bodyB } = event.pairs[i];
// Check if the collision is between a ball and the ground
if ((bodyA && bodyA === this.ground) || (bodyB && bodyB === this.ground)) {
// Game over
this.gameOver();
return;
}
const objectA = bodyA.gameObject;
const objectB = bodyB.gameObject;
// Check if both bodies are balls of the same type..
if (objectA && objectB && objectA.ballType === objectB.ballType) {
this.mergeBalls(objectA, objectB);
}
}
}
“`
Explanation:
– handleCollision(): Detects collisions between balls and merges them if they are of the same level. Also detects if the ball has touched the ground, in which case the jar has overflowed and the game is over.
We’re now writing the method that allows us to merge two balls of the same level into a larger ball of the next level.
“`javascript
mergeBalls(ballA, ballB) {
const nextLevel = BALLS[ballA.ballType.level];
// Update score
this.score += ballA.ballType.radius;
this.scoreText.innerText = this.score;
if (nextLevel) {
// Create a new ball of the next level
this.dropBall((ballA.x + ballB.x) / 2, (ballA.y + ballB.y) / 2, nextLevel);
// Destroy the merged balls
ballA.destroy();
ballB.destroy();
// Remove the balls from the array
this.balls = this.balls.filter(ball => ball !== ballA && ball !== ballB);
} else {
// Clear all balls
this.clearAllBalls();
}
}
“`
Explanation:
– mergeBalls(): Creates a new, larger ball in place of two smaller ones and updates the score.
If the balls reach the maximum level, we’ll clear the jar.
“`javascript
clearAllBalls() {
// Destroy all balls
for (let i = 0; i < this.balls.length; i++) {
const ball = this.balls[i];
if (ball) {
ball.destroy();
}
}
this.balls = []; // Clear the balls array
}
“`
When a ball touches the ground, the game ends. We’ll display a Game Over message and allow the player to restart by pressing any key.
“`javascript
gameOver() {
this.gameStop = true;
// 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’)
.setOrigin(0.5)
.setFontFamily(‘Arial’)
.setFontSize(72)
.setFontStyle(‘bold’)
.setColor(‘red’);
this.add.text(this.centerX, this.centerY + 30, ‘Press any key to restart’)
.setOrigin(0.5)
.setFontFamily(‘Arial’)
.setFontSize(42)
.setColor(‘red’);
}
// Restart the game
restartGame() {
this.scene.restart();
}
“`
Explanation:
– gameOver(): Stops the game and displays the Game Over message.
– restartGame(): Restarts the game when the player presses any key.
“`javascript
const GAME_WIDTH = 1000;
const GAME_HEIGHT = 1000;
// Jar : box dimensions
const BOX_WIDTH = 600;
const BOX_HEIGHT = 700;
const SIDE_WIDTH = 30;
// Walls physics properties
const WALL_PHYSICS = {
density: 1000,
restitution: 0,
friction: 1,
frictionStatic: 1,
isStatic: true,
};
// Ball physics properties
const BALL_PHYSICS = {
restitution: 0,
friction: 0,
density: 1000,
};
// Balls levels data properties : level, radius, sprite
const BALLS = [
{ level: 1, radius: 20, sprite: ‘cap’ },
{ level: 2, radius: 25, sprite: ‘paper_ball’ },
{ level: 3, radius: 40, sprite: ‘newspaper’ },
{ level: 4, radius: 50, sprite: ‘eggshell’ },
{ level: 5, radius: 60, sprite: ‘plastic_bottle’ },
{ level: 6, radius: 70, sprite: ‘coffee’ },
{ level: 7, radius: 80, sprite: ‘glass’ },
{ level: 8, radius: 100, sprite: ‘rotten_fruit’ },
{ level: 9, radius: 130, sprite: ‘dvd’ },
{ level: 10, radius: 180, sprite: ‘basket_ball’ },
];
// Ball generation limit : prevent to generate randoms balls with a high level
const BALL_LEVEL_GENERATION_LIMIT = 5;
class MainScene extends Phaser.Scene {
constructor() {
super({ key: ‘MainScene’ });
}
preload() {
this.load.setBaseURL(‘./assets/’); // Load assets folder
this.load.image(‘cap’, ‘wastes/cap.png’);
this.load.image(‘coffee’, ‘wastes/coffee.png’);
this.load.image(‘dvd’, ‘wastes/dvd.png’);
this.load.image(‘eggshell’, ‘wastes/eggshell.png’);
this.load.image(‘glass’, ‘wastes/glass.png’);
this.load.image(‘newspaper’, ‘wastes/newspaper.png’);
this.load.image(‘paper_ball’, ‘wastes/paper_ball.png’);
this.load.image(‘plastic_bottle’, ‘wastes/plastic_bottle.png’);
this.load.image(‘rotten_fruit’, ‘wastes/rotten_fruit.png’);
this.load.image(‘basket_ball’, ‘wastes/basket_ball.png’);
}
create() {
this.score = 0;
this.balls = []; // Balls in the jar
this.isDropping = false; // If a ball is currently dropping
this.gameStop = false;
this.pointerX = this.centerX;
this.centerX = this.cameras.main.width / 2;
this.centerY = this.cameras.main.height / 2;
this.boxY = this.cameras.main.height – ((BOX_HEIGHT) / 2);
this.leftBox = this.centerX – BOX_WIDTH / 2;
this.rightBox = this.centerX + BOX_WIDTH / 2;
// Create the jar points
const points = this.createWallsBody();
// Create the jar physics body
this.boxBound = this.matter.add.fromVertices(this.centerX, this.boxY, points, WALL_PHYSICS);
// Create the jar graphics
const boxImg = this.add.polygon(this.centerX, this.boxY, points, 0xc78e53);
boxImg.setStrokeStyle(2, 0xb17524);
this.matter.add.gameObject(boxImg, this.boxBound);
// Ground collision detection
this.ground = this.matter.add.rectangle(this.centerX, this.cameras.main.height, this.cameras.main.width, 20, { isStatic: true, isSensor: true });
// Create flying ball
this.createFlyingBall();
// Create score text
this.scoreText = document.getElementById(‘score’);
this.scoreText.innerText = 0;
// Events
this.input.on(‘pointermove’, this.pointermove, this);
this.input.on(‘pointerdown’, this.pointerdown, this);
// Collision detection
this.matter.world.on(‘collisionstart’, this.handleCollision, this);
}
// Create the jar walls from the box dimensions to draw points that will create the shape
// for both physics body and graphics
createWallsBody() {
const innerLeftSide = 0;
const innerRightSide = BOX_WIDTH;
const outerLeftSide = innerLeftSide – SIDE_WIDTH;
const outerRightSide = innerRightSide + SIDE_WIDTH;
const topSide = 0;
const innerBottomSide = topSide + BOX_HEIGHT;
const outerBottomSide = innerBottomSide + SIDE_WIDTH;
return [
{ x: innerLeftSide, y: innerBottomSide },
{ x: innerLeftSide, y: topSide },
{ x: outerLeftSide, y: topSide },
{ x: outerLeftSide, y: outerBottomSide },
{ x: outerRightSide, y: outerBottomSide },
{ x: outerRightSide, y: topSide },
{ x: innerRightSide, y: topSide },
{ x: innerRightSide, y: innerBottomSide },
];
}
createFlyingBall() {
// Create a random ball
const ball = randomBall();
this.currentBall = new FlyingBall(this, this.centerX, 30, ball);
this.updateFlyingBallPosition();
}
pointermove(pointer) {
if (this.gameStop) return;
// Update the flying ball position
this.pointerX = pointer.x;
this.updateFlyingBallPosition();
}
// Update the flying ball position
updateFlyingBallPosition() {
if (!this.currentBall) return;
this.currentBall.setPosition(
Phaser.Math.Clamp(this.pointerX, this.leftBox + this.currentBall.radius, this.rightBox – this.currentBall.radius),
this.currentBall.y
);
}
pointerdown() {
if (this.gameStop || this.isDropping || !this.currentBall) return;
this.isDropping = true;
const [x, y] = [this.currentBall.x, this.currentBall.y];
// Drop the ball
this.dropBall(x, y, this.currentBall.ballType);
// Delete the current ball
this.currentBall.destroy();
// Create the new flying ball
this.time.delayedCall(500, () => {
this.createFlyingBall();
this.isDropping = false;
});
}
// Drop the ball in the jar
dropBall(x, y, type) {
const radius = type.radius; // Get level radius
// Create ball graphics
const ballGraphics = this.matter.add.image(x, y, type.sprite, null, BALL_PHYSICS);
ballGraphics.displayWidth = radius * 2;
ballGraphics.displayHeight = radius * 2;
ballGraphics.ballType = type
ballGraphics.setCircle(radius);
// Push the ball in the array
this.balls.push(ballGraphics);
}
handleCollision(event) {
for (let i = 0, len = event.pairs.length; i < len; i++) {
const { bodyA, bodyB } = event.pairs[i];
// Check if the collision is between a ball and the ground
if ((bodyA && bodyA === this.ground) || (bodyB && bodyB === this.ground)) {
// Game over
this.gameOver();
return;
}
const objectA = bodyA.gameObject;
const objectB = bodyB.gameObject;
// Check if both bodies are balls of the same type..
if (objectA && objectB && objectA.ballType === objectB.ballType) {
this.mergeBalls(objectA, objectB);
}
}
}
mergeBalls(ballA, ballB) {
const nextLevel = BALLS[ballA.ballType.level];
// Update score
this.score += ballA.ballType.radius;
this.scoreText.innerText = this.score;
if (nextLevel) {
// Create a new ball of the next level
this.dropBall((ballA.x + ballB.x) / 2, (ballA.y + ballB.y) / 2, nextLevel);
// Destroy the merged balls
ballA.destroy();
ballB.destroy();
// Remove the balls from the array
this.balls = this.balls.filter(ball => ball !== ballA && ball !== ballB);
} else {
// Clear all balls
this.clearAllBalls();
}
}
// Clear all balls from the jar
clearAllBalls() {
// Destroy all balls
for (let i = 0; i < this.balls.length; i++) {
const ball = this.balls[i];
if (ball) {
ball.destroy();
}
}
this.balls = []; // Clear the balls array
}
gameOver() {
this.gameStop = true;
// 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’)
.setOrigin(0.5)
.setFontFamily(‘Arial’)
.setFontSize(72)
.setFontStyle(‘bold’)
.setColor(‘red’);
this.add.text(this.centerX, this.centerY + 30, ‘Press any key to restart’)
.setOrigin(0.5)
.setFontFamily(‘Arial’)
.setFontSize(42)
.setColor(‘red’);
}
// Restart the game
restartGame() {
this.scene.restart();
}
}
// Flying ball class model
class FlyingBall {
constructor(scene, x, y, ballType) {
this.scene = scene;
this.ballType = ballType;
this.radius = ballType.radius;
// Create the ball graphics
this.graphics = this.scene.add.image(x, y, ballType.sprite);
this.graphics.displayWidth = this.radius * 2;
this.graphics.displayHeight = this.radius * 2;
}
// x getter : return this.graphics.x (alias of this.graphics.x)
get x() {
return this.graphics.x;
}
// y getter : return this.graphics.x (alias of this.graphics.y)
get y() {
return this.graphics.y;
}
setPosition(x, y) {
this.graphics.setPosition(x, y);
}
destroy() {
this.graphics.destroy();
}
}
// Generate a random ball level
function randomBall() {
const level = Phaser.Math.Between(1, BALL_LEVEL_GENERATION_LIMIT);
return BALLS[level – 1];
}
const config = {
type: Phaser.AUTO,
width: GAME_WIDTH,
height: GAME_HEIGHT,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
scene: MainScene,
transparent: true,
antialias: true,
physics: {
default: ‘matter’,
matter: {
gravity: {
x: 0,
y: 1
},
debug: false,
}
},
};
const game = new Phaser.Game(config);
“`
Congratulations! You have built a Suika Game-inspired physics-based game using Phaser and the Matter.js physics engine. You learned how to handle physics, collisions, and player interaction. Feel free to expand the game and add new features!
Here are some ideas to improve and expand the game:
– Add sound effects: Play sound effects when balls collide or merge.
– Play with physics parameters! Have fun modifying the parameters and materials of the various objects and test the different results.
– Add levels: As the game progresses, make the jar smaller or increase the gravity to make the game harder.
– Power-ups: Introduce power-ups, such as balls that split into smaller ones or special abilities that clear the jar.