Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Ballz game, Game development, HTML5, Javascript, Phaser and TypeScript.

The second game in my 101 games challenge will be a Ballz-like game written in TypeScript and powered by Phaser.

By the way, did you already play the first game? It’s Pushori, a tribute to an old Tony Pa’s Flash game. You can play the game a this link and get the free source code on my Gumroad page.

While I am completing the game, I decided to release the source code of the game engine, for you to play a bit and see how it’s done.

Look at the prototype:

Hold and drag to aim, release to shoot. Collect extra balls and don’t let blocks reach the bottom of the stage, or it’s game over.

Compared with old prototypes, I removed custom object pooling and used Phaser’s killAndHide method, plus another set of improvements.

And here it is the completely commented source code, which consists in one HTML file, one CSS file and seven TypeScript files:

index.html

The web page which hosts the game, to be run inside thegame element.

HTML
<!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.

CSS
/* 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.

TypeScript
// CONFIGURABLE GAME OPTIONS
// changing these values will affect gameplay

export const GameOptions : any = {

    gameSize : {
        width               : 646,         // width of the game, in pixels
        height              : 960          // height of the game, in pixels
    },

    gameBackgroundColor     : 0x222222,     // game background color
   
    ballRadius              : 12,           // ball radius, in pixels
    ballBottomMargin        : 100,          // margin from the ball and the bottom of the stage, in pixels
    ballSpeed               : 1000,         // ball speed, in pixels per second

    blocksPerLine           : 7,            // amount of blocks per line
    blockSpacing            : 2,            // spacing between blocks, in pixels
    blockLines              : 8,            // amount of lines in game field
   
    maxBlocksPerLine        : 6,            // maximum amount of blocks per line
    singleSizeProbability   : 80,           // probability of having a single sized block
    doubleSizeProbability   : 15,           // probability of having a double sized block
    quadSizeProbability     : 5,            // probability of having a quadruple sized block
    extraBallProbability    : 80,           // probability of having an extra ball in each row
    sizeMultiplier          : [1, 3, 5],    // block value multiplier, according to block size

    colors                  : [0x2BA4CF, 0x7451C9, 0xF25DA2, 0xFFC30C, 0xFC7417, 0xEC283B, 0x82D523] // possible block colors
}

main.ts

This is where the game is created, with all Phaser related options.

TypeScript
// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';                            // Phaser
import { GameOptions } from './gameOptions';            // game options
import { PreloadAssets } from './scenes/preloadAssets'; // PreloadAssets scene
import { PlayGame } from './scenes/playGame';           // PlayGame scene


// 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
       
    ],
    physics : {                                                                             
        default : 'arcade'                              // physics engine used is arcade physics
    }
}

// the game itself
new Phaser.Game(configObject);

scenes > preloadAssets.ts

Here we preload all assets to be used in the game.

TypeScript
// 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 images
        this.load.image('ball', 'assets/sprites/ball.png');             // the ball 
        this.load.image('trajectory', 'assets/sprites/trajectory.png'); // the trajectory 
        this.load.image('block', 'assets/sprites/block.png');           // the block 
        this.load.image('aimbase', 'assets/sprites/aimbase.png');       // base of the virtual joystick used to aim
        this.load.image('aimstick', 'assets/sprites/aimstick.png');     // top of the virtual joystick used to aim

        // load bitmap fonts
        this.load.bitmapFont('font', 'assets/fonts/font.png', 'assets/fonts/font.fnt'); 
    }
  
    // 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.

TypeScript
// THE GAME ITSELF

// modules to import
import { GameOptions }      from '../gameOptions';                                   
import { ArcadeBall}        from '../arcadeBall';
import { ArcadeBlock }      from '../arcadeBlock';
import { ArcadeExtraBall }  from '../arcadeExtraBall';

// enum to define game states
enum gameState {
    WAITING,        // player is not interacting
    AIMING,         // player is aiming
    READY_TO_FIRE,  // player is ready to fire
    FIRING          // player is firing          
}

// enum to define different block types
enum blockType {
    NORMAL,         // normal block
    DOUBLE,         // 2x1 block
    QUADRUPLE        // 2x2 block
}

// enum to define depth level of various game elements
enum depthLevel {
    BLOCK = 1,      // blocks
    BLOCK_TEXT,     // block text showing its value
    TRAJECTORY,     // line to show ball trajectory 
    VIRTUAL_AIM     // virtual joystick
}

// PlayGame class extends Phaser.Scene class
export class PlayGame extends Phaser.Scene {

    constructor() {
        super({
            key : 'PlayGame'
        });
    }

    blockGroup              : Phaser.Physics.Arcade.StaticGroup; 
    ballGroup               : Phaser.Physics.Arcade.Group; 
    extraBallGroup          : Phaser.Physics.Arcade.StaticGroup; 
    coordsLookupTable       : Phaser.Math.Vector2[][];
    sizeLookupTable         : Phaser.Math.Vector2[];
    freeSpots               : boolean[];
    freeUpperSpots          : boolean[];
    dummyBall               : Phaser.GameObjects.Sprite;

    // method to be called once the instance has been created
    create() : void {

        // we start from level 1
        let level : number = 1;

        // no landed balls at the moment
        let landedBalls : number = 0;

        // trajectory direction
        let direction : number = 0;

        let landingX : number = this.game.config.width as number / 2;

        // virtual joystick
        const aimBase : Phaser.GameObjects.Sprite = this.add.sprite(0, 0, 'aimbase');
        aimBase.setVisible(false);
        aimBase.setDepth(depthLevel.VIRTUAL_AIM);
        const aimStick : Phaser.GameObjects.Sprite = this.add.sprite(0, 0, 'aimstick');
        aimStick.setVisible(false);
        aimStick.setDepth(depthLevel.VIRTUAL_AIM);        

        // set all free spots to true
        this.freeUpperSpots = [];
        this.freeSpots = [];
        for (let i : number = 0; i < GameOptions.blocksPerLine; i ++) {
            this.freeSpots[i] = true;
            this.freeUpperSpots[i] = true;
        }

        // some constants to adapt window size to game size
        const totalHorizontalPadding : number = GameOptions.blockSpacing * (GameOptions.blocksPerLine + 1);
        const totalVerticalPadding : number = GameOptions.blockSpacing * (GameOptions.blockLines + 1);
        const blockSize : number = (GameOptions.gameSize.width - totalHorizontalPadding) / GameOptions.blocksPerLine;
        const blockAndSpacingSize : number = blockSize + GameOptions.blockSpacing;
        const fieldHeight : number = totalVerticalPadding + blockSize * GameOptions.blockLines;
        const worldStartX : number = (this.game.config.width as number - GameOptions.gameSize.width) / 2;
        const worldStartY : number = this.game.config.height as number - fieldHeight - GameOptions.ballBottomMargin;

        // actual game field rectangle, the arcade physics world
        const fieldRectangle : Phaser.Geom.Rectangle = new Phaser.Geom.Rectangle(worldStartX, worldStartY, GameOptions.gameSize.width, fieldHeight);
        
        // optionalyl draw game field rectangle
        this.add.graphics().lineStyle(1, 0xff0000).strokeRectShape(fieldRectangle);

        // set physics world
        this.physics.world.setBounds(fieldRectangle.left, fieldRectangle.top, fieldRectangle.width, fieldRectangle.height);
        
        // add physics groups
        this.ballGroup = this.physics.add.group({
            collideWorldBounds : true,
            bounceX : 1,
            bounceY : 1
        });
        this.blockGroup = this.physics.add.staticGroup();
        this.extraBallGroup = this.physics.add.staticGroup();

        // lookup table for blocks coordinates
        this.coordsLookupTable = [[], [], []];
        for (let i = 0; i <= GameOptions.blocksPerLine; i ++) {
            this.coordsLookupTable[blockType.NORMAL][i] = new Phaser.Math.Vector2(fieldRectangle.left + i * blockAndSpacingSize + GameOptions.blockSpacing + blockSize / 2, fieldRectangle.top - blockSize / 2);
            this.coordsLookupTable[blockType.DOUBLE][i] = new Phaser.Math.Vector2(fieldRectangle.left + i * blockAndSpacingSize + GameOptions.blockSpacing + blockSize + GameOptions.blockSpacing / 2, fieldRectangle.top - blockSize / 2);
            this.coordsLookupTable[blockType.QUADRUPLE][i] = new Phaser.Math.Vector2(fieldRectangle.left + i * blockAndSpacingSize + GameOptions.blockSpacing + blockSize + GameOptions.blockSpacing / 2, fieldRectangle.top - blockSize - GameOptions.blockSpacing / 2);
        }

        // lookup table for block size coordinates
        this.sizeLookupTable = [
            new Phaser.Math.Vector2(blockSize, blockSize),
            new Phaser.Math.Vector2(blockSize * 2 + GameOptions.blockSpacing, blockSize),
            new Phaser.Math.Vector2(blockSize * 2 + GameOptions.blockSpacing, blockSize * 2 + GameOptions.blockSpacing),
        ]

        //this.add.tileSprite(fieldRectangle.left - 1, 0, 8, gameHeight, 'divider').setOrigin(1, 0).setDepth(2);
        //this.add.tileSprite(fieldRectangle.right + 1, 0, 8, gameHeight, 'divider').setOrigin(0, 0).setDepth(2);
        //this.add.tileSprite(fieldRectangle.left, fieldRectangle.bottom + 1, fieldRectangle.width, GameOptions.ballBottomMargin, 'filler').setOrigin(0).setDepth(2);
        //this.add.tileSprite(fieldRectangle.left, 0, fieldRectangle.width, fieldRectangle.top - 1, 'filler').setOrigin(0).setDepth(2);

        this.data.set({
            state       : gameState.WAITING, // at the beginning, game state is MOVING because we want the player to move
            movingStuff : 0,
            level       : 1              
        });

        // add first arcade ball and dummy ball
        new ArcadeBall(this, fieldRectangle.centerX, fieldRectangle.bottom - GameOptions.ballRadius, this.ballGroup);
        this.dummyBall = this.add.sprite(fieldRectangle.centerX, fieldRectangle.bottom - GameOptions.ballRadius, 'ball');
        
        // trajectory
        const trajectory : Phaser.GameObjects.TileSprite = this.add.tileSprite(fieldRectangle.centerX, fieldRectangle.bottom - GameOptions.ballRadius * 2, 12, 480, 'trajectory');
        trajectory.setOrigin(0.5, 1);
        trajectory.setVisible(false);
        trajectory.setDepth(depthLevel.TRAJECTORY);

        // triggered when the input is down
        this.input.on('pointerdown', (pointer : Phaser.Input.Pointer) => {
            if (this.data.get('state') == gameState.WAITING) {
                this.data.set('state', gameState.AIMING);
                aimBase.setPosition(pointer.x, pointer.y);
                aimBase.setVisible(true);
                trajectory.setX(this.dummyBall.x);   
            }
        });

        // triggered when the input is moved
        this.input.on('pointermove', (pointer : Phaser.Input.Pointer) => {
            if (this.data.get('state') == gameState.AIMING || this.data.get('state') == gameState.READY_TO_FIRE) {
                const distY : number = pointer.y - pointer.downY;
                if (distY > 10) {
                    this.data.set('state', gameState.READY_TO_FIRE);
                    trajectory.setVisible(true);
                    aimStick.setVisible(true);
                    aimStick.setPosition(pointer.x, pointer.y);
                    direction = Phaser.Math.Angle.Between(pointer.x, pointer.y, pointer.downX, pointer.downY);
                    trajectory.setRotation(direction + Math.PI / 2);
                    trajectory.setPosition(this.ballGroup.getFirstAlive().x + GameOptions.ballRadius * 2 * Math.cos(direction), this.ballGroup.getFirstAlive().y + GameOptions.ballRadius * 2 * Math.sin(direction))
                }
                else {
                    this.data.set('state', gameState.AIMING);    
                    trajectory.setVisible(false);
                }
            }
        });

        // triggered whe the pointer is released
        this.input.on('pointerup', () => {
            aimBase.setVisible(false);
            aimStick.setVisible(false);
            trajectory.setVisible(false);  
            if (this.data.get('state') == gameState.READY_TO_FIRE) {
                this.data.set('state', gameState.FIRING)
                landedBalls = 0;
                this.ballGroup.getChildren().forEach((ball : Phaser.GameObjects.GameObject, index : number) => {
                    const ballSprite : ArcadeBall = ball as ArcadeBall;
                    this.time.addEvent({
                        delay : 100 * index,
                        callback : () => {
                            ballSprite.fire(direction, GameOptions.ballSpeed);
                            if (index == this.ballGroup.countActive() - 1) {
                                this.dummyBall.setVisible(false);
                            }
                        }
                    })   
                });
            }
            else {
                if (this.data.get('state') != gameState.FIRING) {
                    this.data.set('state', gameState.WAITING);  
                }  
            }
        });

        // collision on world bounds
        this.physics.world.on('worldbounds', (body : Phaser.Physics.Arcade.Body, up : boolean, down : boolean, left : boolean, right : boolean) => {
            if (down && this.data.get('state') == gameState.FIRING) {
                body.setVelocity(0);
                if (landedBalls == 0) {
                    landingX = body.center.x;
                }
                landedBalls ++;
                if (landedBalls == this.ballGroup.countActive()) {
                    this.moveBlocks(blockAndSpacingSize); 
                    this.moveBalls(blockAndSpacingSize, landingX);              
                }
            }
        });

        // ball Vs group
        this.physics.add.collider(this.ballGroup, this.blockGroup, (ball : any, block : any) => {
            if (this.data.get('state') == gameState.FIRING) {
                block.hit(this.blockGroup);
            }
        });

        // ball Vs extra ball
        this.physics.add.overlap(this.ballGroup, this.extraBallGroup, (ball : any, extraBall : any) => {
            if (this.data.get('state') == gameState.FIRING) {
                extraBall.fallDown(fieldRectangle.bottom - GameOptions.ballRadius);
            }
        });

        this.addBlockLine(level);
        this.moveBlocks(blockAndSpacingSize);
        this.moveBalls(blockAndSpacingSize, landingX);
       
    }

    // method to add a block line
    addBlockLine(level : number) : void {

        // handle free spots
        for (let i : number = 0; i < GameOptions.blocksPerLine; i ++) {
            this.freeSpots[i] = this.freeUpperSpots[i];
            this.freeUpperSpots[i] = true;   
        }

        // extra ball
        const placeExtraBall : boolean = Phaser.Math.RND.integerInRange(1, 100) < GameOptions.extraBallProbability;
        if (placeExtraBall) {
            const ballPosition : number = Phaser.Math.RND.integerInRange(0, GameOptions.blocksPerLine - 1);
            const ballX : number = this.coordsLookupTable[blockType.NORMAL][ballPosition].x;
            const ballY : number = this.coordsLookupTable[blockType.NORMAL][ballPosition].y;
            if (this.freeSpots[ballPosition]) {
                this.freeSpots[ballPosition] = false;
                const newExtraBall : ArcadeExtraBall | null = this.extraBallGroup.getFirstDead();
                if (newExtraBall == null) {
                    const ball : ArcadeExtraBall = new ArcadeExtraBall(this, ballX, ballY);
                    this.extraBallGroup.add(ball);
                }
                else {
                    newExtraBall.putInGame(ballX, ballY);
                }
            }
        }

        const color : number = Phaser.Math.RND.pick(GameOptions.colors); 
        const placedBlocks : number[] = [];
        for (let i = 0; i < GameOptions.maxBlocksPerLine; i ++) {
            const blockPosition : number = Phaser.Math.RND.integerInRange(0, GameOptions.blocksPerLine - 1);
            if (placedBlocks.indexOf(blockPosition) == -1 && this.freeSpots[blockPosition]) {
                let type : blockType = blockType.NORMAL;
                if (blockPosition < GameOptions.blocksPerLine - 1 && placedBlocks.indexOf(blockPosition + 1) == -1) {
                    const randomNumber : number = Phaser.Math.RND.integerInRange(1, 100);    
                    if (randomNumber < GameOptions.quadSizeProbability && this.freeSpots[blockPosition + 1]) {
                        type = blockType.QUADRUPLE;
                        this.freeUpperSpots[blockPosition] = false;
                        this.freeUpperSpots[blockPosition + 1] = false;
                    }
                    else {
                        if (randomNumber < GameOptions.doubleSizeProbability && this.freeSpots[blockPosition + 1]) {
                            type = blockType.DOUBLE;
                        }
                    }
                }
                placedBlocks.push(blockPosition);
                if (type == blockType.DOUBLE || type == blockType.QUADRUPLE) {
                    placedBlocks.push(blockPosition + 1);    
                }  

                // place the block
                const blockX : number = this.coordsLookupTable[type][blockPosition].x;
                const blockY : number = this.coordsLookupTable[type][blockPosition].y;
                const newBlock : ArcadeBlock | null = this.blockGroup.getFirstDead();
                if (newBlock == null) {
                    const block = new ArcadeBlock(this, blockX, blockY, this.sizeLookupTable[type], depthLevel.BLOCK, depthLevel.BLOCK_TEXT, level * GameOptions.sizeMultiplier[type], color, this.blockGroup);    
                }
                else {
                    newBlock.putInGame(blockX, blockY, level, this.sizeLookupTable[type], color);
                }                
            }
        }
    }

    // method to move blocks
    moveBlocks(deltaY : number) : void {
        this.data.inc('movingStuff');
        this.tweens.add({
            targets : this.blockGroup.getChildren(),
            props : {
                y : {
                    getEnd : (target) => {
                        return target.y + deltaY;
                    }
                }
            },
            duration : 500,
            ease : Phaser.Math.Easing.Cubic.InOut,
            onUpdate : (tween : any, target : any) => {
                target.updateText();
            },
            onComplete : () => {
                this.data.inc('movingStuff', -1);
                if (this.data.get('movingStuff') == 0) {
                    this.prepareNewTurn();
                }
            }
        })
    }

    // method to move balls
    moveBalls(deltaY : number, destinationX : number) : void {
        this.data.inc('movingStuff');
        this.tweens.add({
            targets : this.extraBallGroup.getChildren(),
            props : {
                y : {
                    getEnd : (target) => {
                        if (target.collected) {
                            return target.y    
                        }
                        return target.y + deltaY;
                    }
                },
                x : {
                    getEnd : (target) => {
                        if (target.collected) {
                            return destinationX;
                        }
                        else {
                            return target.x;
                        }
                    }    
                }
            },
            duration : 500,
            ease : Phaser.Math.Easing.Cubic.InOut,
            onComplete : () => {
                this.data.inc('movingStuff', -1);
                if (this.data.get('movingStuff') == 0) {
                    this.prepareNewTurn();
                }
            }
        })
        this.data.inc('movingStuff');
        this.tweens.add({
            targets : this.ballGroup.getChildren(),
            x : destinationX,    
            duration : 500,
            ease : Phaser.Math.Easing.Cubic.InOut,
            onComplete : () => {
                this.data.inc('movingStuff', -1);
                if (this.data.get('movingStuff') == 0) {
                    this.prepareNewTurn();
                }  
            }
        })
    }

    // method to prepare a new turn
    prepareNewTurn() : void {
        for (let block of this.blockGroup.getChildren()) {
            const actualBlock : ArcadeBlock = block as ArcadeBlock;
            if (actualBlock.updateAndCheck(GameOptions.blockLines - 1)) {
                this.scene.restart();
                break; 
            }    
        } 
        this.data.inc('level');
        this.addBlockLine(this.data.get('level'));  
        this.blockGroup.refresh();
        this.data.set('state', gameState.WAITING); 
        this.extraBallGroup.getChildren().forEach((extraBall : any) => {
            if (extraBall.collected && extraBall.active) {
                const ball : ArcadeBall = new ArcadeBall(this, extraBall.x, extraBall.y, this.ballGroup);
                this.extraBallGroup.killAndHide(extraBall);  
            }
        })
        this.extraBallGroup.refresh(); 
        this.ballGroup.getChildren().forEach((ball : any, index : number) => {
            if (index == 0) {
                this.dummyBall.setX(ball.x);
                this.dummyBall.setVisible(true);
            }
            ball.setVisible(false)
        });
    }
}

arcadeBall.ts

Custom class to define the ball.

TypeScript
// ArcadeBall class extends Phaser.Physics.Arcade.Sprite
export class ArcadeBall extends Phaser.Physics.Arcade.Sprite {

    body : Phaser.Physics.Arcade.Body;

    constructor(scene : Phaser.Scene, posX : number, posY : number, group : Phaser.Physics.Arcade.Group) {
        super(scene, posX, posY, 'ball');
        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.body.onWorldBounds = true;
        group.add(this);
        this.setVisible(false);
    }

    // method to fire the ball
    fire(direction : number, velocity : number) : void {
        this.body.setVelocity(velocity * Math.cos(direction), velocity * Math.sin(direction));   
        this.setVisible(true);
    }

    // method to make the ball land
    land() : void {
        this.body.setVelocity(0);
    }
}

arcadeBlock.ts

Custom class to define the block.

TypeScript
// ArcadeBlock class extends Phaser.GameObjects.NineSlice
export class ArcadeBlock extends Phaser.GameObjects.NineSlice {

    body        : Phaser.Physics.Arcade.Body;
    value       : number;
    row         : number;  
    valueText   : Phaser.GameObjects.BitmapText;

    constructor(scene : Phaser.Scene, posX : number, posY : number, size : Phaser.Math.Vector2, depth : number, textDepth : number, level : number, color : number, group : Phaser.Physics.Arcade.StaticGroup) {
        super(scene, posX, posY, 'block', 0, size.x, size.y, 10, 10, 10, 10);
        scene.add.existing(this);
        this.row = -1;
        this.value = 1;
        this.setDepth(depth);
        this.valueText = this.scene.add.bitmapText(posX, posY, 'font', this.value.toString(), 32);
        this.valueText.setDepth(textDepth);
        this.valueText.setOrigin(0.5);
        this.value = level;
        this.valueText.setText(level.toString());   
        this.setTint(color);
        group.add(this);
    }

    // method to be executed each time the block is hit
    hit(group : Phaser.Physics.Arcade.StaticGroup) : void {
        this.value --;
        this.valueText.setText(this.value.toString());
        if (this.value == 0) {
            this.body.checkCollision.none = true;
            group.killAndHide(this);
            this.valueText.setVisible(false);
        }  
    }

    // method to update text position
    updateText() : void {
        this.valueText.setPosition(this.x, this.y);
    }

    // method to put the block back in game
    putInGame(x : number, y : number, level : number, size : Phaser.Math.Vector2, color : number) : void {
        this.setActive(true);
        this.setPosition(x, y);
        this.setVisible(true);
        this.setTint(color);
        this.setSize(size.x, size.y);
        this.body.setSize(size.x, size.y);
        this.valueText.setVisible(true);
        this.valueText.setText(level.toString());
        this.value = level;
        this.row = -1;
        this.updateText();
        this.body.checkCollision.none = false;
    }

    // method to update block row and check if it's game over
    updateAndCheck(limit : number) : boolean {
        if (!this.active) {
            return false;
        }
        this.row ++;
        return this.row == limit;
    }
}

arcadeExtraBall.ts

Custom class to define the extra ball.

TypeScript
// ArcadeExtraBall class extends Phaser.GameObjects.Sprite
export class ArcadeExtraBall extends Phaser.GameObjects.Sprite {

    collected : boolean;
    body : Phaser.Physics.Arcade.Body;

    constructor(scene : Phaser.Scene, posX : number, posY : number) {
        super(scene, posX, posY, 'ball');
        scene.add.existing(this);
        this.collected = false;
    }

    // method to make extra ball fall down, when collected
    fallDown(destinationY : number) : void {
        if (!this.collected) {
            this.body.checkCollision.none = true;
            this.collected = true;
            this.scene.tweens.add({
                targets : this,
                y : destinationY,
                duration : 500,
                ease : Phaser.Math.Easing.Cubic.Out
            })
        }
    }

    // method to put the ball back in play
    putInGame(x : number, y : number) : void {
        this.setActive(true);
        this.setPosition(x, y);
        this.setVisible(true);
        this.collected = false;
        this.body.checkCollision.none = false;
    }

}

Now you are ready to write your own Ballz game, download the entire project and build something useful out of it.

Don’t know where to start? I have a free guide for you.

Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.