Talking about Zuma game, Game development, HTML5, Javascript, Phaser and TypeScript.
Do you remember Zuma? It’s a tile-matching puzzle video game developed by PopCap Games. Released in 2003, it gained popularity for its simple yet addictive gameplay.
The goal is to eliminate all the balls in a chain before they reach the end of a path. If a ball reaches the end, it’s game over.
I was asked by a reader to make a prototype of Zuma, so why not?
Making it with Phaser is super easy, as it has excellent path management.
All we have to do, at first, is to draw a path and have the gems run on it.
Phaser’s path management provides that given a given instant t, ranging from 0 to 1, where 0 represents the beginning of the path and 1 the end, we are able to know where we are on the path.
Look at the gems running through the path:
No interaction at the moment, I am going to add it later.
This is the completely commented source code, which consists in one HTML file, one CSS file and four TypeScript files:
index.html
The web page which hosts the game, to be run inside thegame element.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="style.css">
<script src="main.js"></script>
</head>
<body>
<div id = "thegame"></div>
</body>
</html>
style.css
The cascading style sheets of the main web page.
/* remove margin and padding from all elements */
* {
padding : 0;
margin : 0;
}
/* set body background color */
body {
background-color : #000000;
}
/* Disable browser handling of all panning and zooming gestures. */
canvas {
touch-action : none;
}
gameOptions.ts
Configurable game options.
It’s a good practice to place all configurable game options, if possible, in a single and separate file, for a quick tuning of the game. I also grouped the variables to keep them more organized.
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay
export const GameOptions : any = {
gameSize : {
width : 1920, // width of the game, in pixels
height : 1080 // height of the game, in pixels
},
gameBackgroundColor : 0x222222, // game background color
path : '{"type":"Path","x":0,"y":0,"autoClose":false,"curves":[{"type":"LineCurve","points":[1460,-50,1460,540]},{"type":"EllipseCurve","x":960,"y":540,"xRadius":500,"yRadius":500,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":450,"yRadius":450,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":400,"yRadius":400,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":350,"yRadius":350,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":300,"yRadius":300,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":250,"yRadius":250,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":200,"yRadius":200,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":910,"y":540,"xRadius":150,"yRadius":150,"startAngle":180,"endAngle":0,"clockwise":false,"rotation":0},{"type":"EllipseCurve","x":960,"y":540,"xRadius":100,"yRadius":100,"startAngle":0,"endAngle":180,"clockwise":false,"rotation":0}]}',
gemSpeed : 400, // gem speed, in pixels per second
gemRadius : 24 // gem radius, in pixels
}
main.ts
This is where the game is created, with all Phaser related options.
// MAIN GAME FILE
// modules to import
import Phaser from 'phaser'; // Phaser
import { PreloadAssets } from './scenes/preloadAssets'; // preloadAssets scene
import { PlayGame } from './scenes/playGame'; // playGame scene
import { GameOptions } from './gameOptions'; // game options
// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
mode : Phaser.Scale.FIT, // adjust size to automatically fit in the window
autoCenter : Phaser.Scale.CENTER_BOTH, // center the game horizontally and vertically
parent : 'thegame', // DOM id where to render the game
width : GameOptions.gameSize.width, // game width, in pixels
height : GameOptions.gameSize.height // game height, in pixels
}
// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
type : Phaser.AUTO, // game renderer
backgroundColor : GameOptions.gameBackgroundColor, // game background color
scale : scaleObject, // scale settings
scene : [ // array with game scenes
PreloadAssets, // PreloadAssets scene
PlayGame // PlayGame scene
]
}
// the game itself
new Phaser.Game(configObject);
scenes > preloadAssets.ts
Here we preload all assets to be used in the game.
// CLASS TO PRELOAD ASSETS
// PreloadAssets class extends Phaser.Scene class
export class PreloadAssets extends Phaser.Scene {
// constructor
constructor() {
super({
key : 'PreloadAssets'
});
}
// method to be called during class preloading
preload() : void {
// load image
this.load.image('gem', 'assets/sprites/gem.png');
}
// method to be executed when the scene is created
create() : void {
// start PlayGame scene
this.scene.start('PlayGame');
}
}
scenes >playGame.ts
Main game file, all game logic is stored here.
// THE GAME ITSELF
// modules to import
import { GameOptions } from '../gameOptions';
// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {
constructor() {
super({
key : 'PlayGame'
});
}
graphics : Phaser.GameObjects.Graphics; // graphics object where to render the path
path : Phaser.Curves.Path; // the path
gems : Phaser.GameObjects.Sprite[]; // array with all gems
// method to be called once the instance has been created
create() : void {
// initialize gems array
this.gems = [];
// create the path and load curves from a JSON string
this.path = new Phaser.Curves.Path(0, 0);
this.path.fromJSON(JSON.parse(GameOptions.path));
// get path length, in pixels
this.data.set('pathLength', this.path.getLength());
// add the graphic object and draw the path on it
this.graphics = this.add.graphics();
this.graphics.lineStyle(2, 0xffffff, 1);
this.path.draw(this.graphics);
// add a gem
this.addGem(0);
}
// method to add a gem
// t : time relative to path, from 0 to 1, where 0 = at the beginning of the path, and 1 = at the end of the path
addGem(t : number) : void {
// get gem start point
const startPoint : Phaser.Math.Vector2 = this.path.getPoint(t);
// create a sprite at gem start point
const gemSprite : Phaser.GameObjects.Sprite = this.add.sprite(startPoint.x, startPoint.y, 'gem');
// set a custom "t" property
gemSprite.setData('t', t);
// add gem sprite to gemSprite array
this.gems.push(gemSprite);
}
// metod to be called at each frame
// time : time passed since the beginning, in milliseconds
// deltaTime : time passed since last frame, in milliseconds
update(time : number, deltaTime : number) {
// determine delta t movement
const deltaT : number = deltaTime / 1000 * GameOptions.gemSpeed / this.data.get('pathLength');
// loop through all gems
this.gems.forEach((gem : Phaser.GameObjects.Sprite, index : number) => {
// update gem's t data
gem.setData('t', gem.getData('t') + deltaT);
// if the gem reached the end of the path
if (gem.getData('t') > 1) {
// restart the game
this.scene.start('PlayGame');
}
// if gem did not reach the end of the path
else {
// get new gem path point
const pathPoint : Phaser.Math.Vector2 = this.path.getPoint(gem.getData('t'));
// move the gem to new path point
gem.setPosition(pathPoint.x, pathPoint.y);
// get travelled distance , in pixels
const travelledDistance : number = this.data.get('pathLength') * gem.getData('t');
// if this is the last gem and there's enough space for another gem
if (index == this.gems.length - 1 && travelledDistance > GameOptions.gemRadius * 2) {
// add a gem right behind it
this.addGem((travelledDistance - GameOptions.gemRadius * 2) / this.data.get('pathLength'));
}
}
})
}
}
Making sprites follow a path was easy, so in next step I am adding some new features. Meanwhile, get the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.