Get the full commented source code of

HTML5 Suika Watermelon Game

Talking about Block it game, Box2D, Game development, HTML5, Javascript, Phaser and TypeScript.

After some time and various ways to manage the physics, we are ready to turn the HTML5 “Block it” prototype into something playable.

As usual, I am using Phaser and Box2D powered by Planck.js.

Let’s made a small recap of the Box2D version: first I built the basic prototype with compound objects and listeners, but the compound object was rendered using a graphic object.

So in previous step some sprites have been used to display the compound object.

Now it’s time to add player interaction, so you can turn on and off the walls by tapping or untapping the cavas. The more you activate the walls, the more energy you consume, until it’s over.

Have a look at the example:

Tap the canvas to activate the walls when the ball is about to hit them, but don’t waste too much energy.

Remember to activate walls before the ball hits them, or it’s game over.

Let’s see the source code, made of one html file, one css file and 7 TypeScript files.

index.html

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

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        </style>
        <script src="main.js"></script>
    </head>
    <body>
        <div id="thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

canvas {
    touch-action : none;
    -ms-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.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {

    // unit used to convert pixels to meters and meters to pixels
    worldScale : 30,

    // ball radius, in pixels
    ballRadius : 25,

    // ball start speed, in meters/second
    ballStartSpeed : 2,

    // ball speed increase at each collision, in meters/second
    ballSpeedIncrease : 0.3,

    // wall width, in pixels
    wallTickness : 20,

    // wall edge distance from center, in pixels
    wallDistanceFromCenter : 350,

    // game energy, that is the amount in milliseconds of active wall time
    energy : 5000
}

main.ts

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

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 750,
    height : 750
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0xfe5430,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

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

preloadAssets.ts

Here we preload all assets to be used in the game, such as the sprites used for the ball and the wall.

// CLASS TO PRELOAD ASSETS

// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {

    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }

    // method to be execute during class preloading
    preload(): void {

        // this is how we preload an image
        this.load.image('ball', 'assets/ball.png');
        this.load.image('wall', 'assets/wall.png');
	}

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

        // call PlayGame class
        this.scene.start('PlayGame');
	}
}

planckUtils.ts

Just a couple of functions because Box2D works with meters while Phaser and most frameworks work with pixels. Here is where we convert pixels to meters and meters to pixels.

import { GameOptions } from './gameOptions';
 
// simple function to convert pixels to meters
export function toMeters(n : number) : number {
    return n / GameOptions.worldScale;
}
 
// simple function to convert meters to pixels
export function toPixels(n: number) : number {
    return n * GameOptions.worldScale;
}

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import * as planck from 'planck';
import { GameOptions } from './gameOptions';
import { PlanckCompound } from './planckCompound';
import { PlanckBall } from './planckBall';
import { toPixels } from './planckUtils';

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

    // Box2d world
    world : planck.World;

    // the ball
    theBall : PlanckBall;

    // planck compound object
    theCompound : PlanckCompound;

    // graphics objects to render the compound object
    compoundGraphics : Phaser.GameObjects.Graphics;

    // keep track of the number of bounces to restart the demo at 50
    bounces : number;

    // flag to check if it's game over
    gameOver : boolean;

    // variable to save the time the player touched the input to activate the wall
    downTime : number;

    // walls energy
    energy : number;

    // text object to display the energy
    energyText : Phaser.GameObjects.Text;

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

    // method to be executed when the scene has been created
    create() : void {

        // it's not game over... yet
        this.gameOver = false;

        // fill the energy according to game options
        this.energy = GameOptions.energy;

        // zero bounces at the beginning
        this.bounces = 0

        // just a couple of variables to store game width and height
        let gameWidth : number = this.game.config.width as number;
        let gameHeight : number = this.game.config.height as number;
    
        // world gravity, as a Vec2 object. It's just a x, y vector
        let gravity = new planck.Vec2(0, 0); 
         
        // this is how we create a Box2D world
        this.world = new planck.World(gravity);

        // add the planck ball
        this.theBall = new PlanckBall(this, this.world, gameWidth / 2, gameHeight / 2, GameOptions.ballRadius, 'ball');

        // give the ball a random velocity
        this.theBall.setRandomVelocity(GameOptions.ballStartSpeed);

        // add the planck compound object
        this.theCompound = new PlanckCompound(this, this.world, gameWidth / 2, gameHeight / 2, GameOptions.wallDistanceFromCenter, GameOptions.wallTickness);

        // add simulation graphics
        this.compoundGraphics = this.add.graphics();
        
        // add an event listener waiting for a contact to solve
        this.world.on('post-solve', () => {

            // handle ball bounce increasing speed
            this.theBall.handleBounce(this.theBall.angleToReflect, GameOptions.ballSpeedIncrease);
            
            // increase number of bounces
            this.bounces ++;

        })

        // add an event listener waiting for a contact to pre solve
        this.world.on('pre-solve', (contact : planck.Contact) => {

            // if the compound is not active or if it's game over...
            if (!this.theCompound.active || this.gameOver) {

                // do not enable the contact
                contact.setEnabled(false);

                // if it's not game over...
                if (!this.gameOver) {

                    // ...now it is! :)
                    this.gameOver = true;

                    // add a time event
                    this.time.addEvent({

                        // start in 2 seconds
                        delay : 2000,

                        // callback function scope
                        callbackScope : this,

                        // callback function
                        callback: () => {

                            // restart the game
                            this.scene.start('PlayGame');
                        }
                    });
                }

                // exit the method
                return;     
            }

            // get edge fixture in this circle Vs edge contact
            let edgeFixture : planck.Fixture = this.getEdgeFixture(contact);

            // get edge body
            let edgeBody : planck.Body = edgeFixture.getBody();

            // get edge shape
            let edgeShape : planck.Edge = edgeFixture.getShape() as planck.Edge;

            // did the ball just collided with this edge?
            if (this.theBall.sameEdgeCollision(edgeShape)) {
                
                // disable the contact
                contact.setEnabled(false);

                // exit callback function
                return;    
            }

            // get edge shape vertices
            let worldPoint1 : planck.Vec2 = edgeBody.getWorldPoint(edgeShape.m_vertex1);
            let worldPoint2 : planck.Vec2 = edgeBody.getWorldPoint(edgeShape.m_vertex2);
            
            // transform the planck edge into a Phaser line
            let worldLine : Phaser.Geom.Line = new Phaser.Geom.Line(toPixels(worldPoint1.x), toPixels(worldPoint1.y), toPixels(worldPoint2.x), toPixels(worldPoint2.y));
            
            // determine bounce angle
            this.theBall.determineBounceAngle(worldLine); 
        });
        
        // add the text object to show the amount of energy
        this.energyText = this.add.text(10, 10, this.energy.toString(), {
            font : '64px Arial'
        });

        // wait for input start to call activateWalls method
        this.input.on('pointerdown', this.activateWalls, this);

        // wait for input end to call deactivateWalls method
        this.input.on('pointerup', this.deactivateWalls, this); 
    }

    // method to update energy text
    updateEnergyText(energy : number) : void {

        // set energy text to energy value itself or zero
        this.energyText.setText(Math.max(0, energy).toString());
    }

    // method to activate compound walls
    activateWalls(e : Phaser.Input.Pointer) : void {

        // do we have energy?
        if (this.energy > 0) {

            // set compound walls to active
            this.theCompound.setActive(true);

            // save the current timestamp
            this.downTime = e.downTime;
        }
    }

    // method to deactivate compound walls
    deactivateWalls(e : Phaser.Input.Pointer) : void {

        // set compound walls to inactive
        this.theCompound.setActive(false);

        // determine how long the input has been pressed
        let ellapsedTime : number = Math.round(e.upTime - e.downTime);
        
        // subtract the ellapsed time from energy
        this.energy -= ellapsedTime;

        // update energy text
        this.updateEnergyText(this.energy);
    }

    // method to get the edge fixture in a edge Vs circle contact
    getEdgeFixture(contact : planck.Contact) : planck.Fixture {

        // get first contact fixture
        let fixtureA : planck.Fixture = contact.getFixtureA(); 

        // get first contact shape
        let shapeA : planck.Shape = fixtureA.getShape();

        // is the shape an edge? Return the first contact fixture, else return second contact fixture
        return (shapeA.getType() == 'edge') ? fixtureA : contact.getFixtureB();
    }

    // method to be called at each frame
    update(time : number) : void {

        // is the compound object active?
        if (this.theCompound.active) {

            // determine remaining energy subtracting from current energy the amount of time we are pressing the input
            let remainingEnergy : number = this.energy - Math.round(time - this.downTime);

            // if remaining energy is less than zero...
            if (remainingEnergy <= 0) {

                // set energy to zero
                this.energy = 0;

                // turn off the compound
                this.theCompound.setActive(false);
            }

            // update energy text
            this.updateEnergyText(remainingEnergy);
        }

        // advance the simulation by 1/30 seconds
        this.world.step(1 / 30);
  
        // crearForces  method should be added at the end on each step
        this.world.clearForces();

        // update ball position
        this.theBall.updatePosition();
        
        // draw walls of compound object
        this.theCompound.drawWalls(this.compoundGraphics);
    }
}

planckBall.ts

Custom class extending Phaser.GameObjects.Sprite to define the planck ball as a Phaser sprite.

import * as planck from 'planck';
import { toMeters, toPixels } from './planckUtils';
 
// this class extends planck Phaser Sprite class
export class PlanckBall extends Phaser.GameObjects.Sprite {

    // planck body
    planckBody : planck.Body;
    
    // ball speed
    speed : number;

    // last collided edge, to prevent colliding with the same edge twice
    lastCollidedEdge : planck.Edge;

    // angle to reflect after a collision
    angleToReflect : number;
 
    constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, radius : number, key : string) {
 
        super(scene, posX, posY, key);
         
        // adjust sprite display width and height
        this.displayWidth = radius * 2;
        this.displayHeight = radius * 2;
 
        // add sprite to scene
        scene.add.existing(this);
     
        // this is how we create a generic Box2D body
        this.planckBody  = world.createBody();

        // set the body as bullet for continuous collision detection
        this.planckBody.setBullet(true)
 
        // Box2D bodies are created as static bodies, but we can make them dynamic
        this.planckBody.setDynamic();
 
        // a body can have one or more fixtures. This is how we create a circle fixture inside a body
        this.planckBody.createFixture(planck.Circle(toMeters(radius)));
  
        // now we place the body in the world
        this.planckBody.setPosition(planck.Vec2(toMeters(posX), toMeters(posY)));

        // time to set mass information
        this.planckBody.setMassData({
 
            // body mass
            mass : 1,
 
            // body center
            center : planck.Vec2(),
  
            // I have to say I do not know the meaning of this "I", but if you set it to zero, bodies won't rotate
            I : 1
        });
    }

    // method to set a random velocity, given a speed
    setRandomVelocity(speed : number) : void {

        // save the speed as property
        this.speed = speed;
         
        // set a random angle
         let angle : number = Phaser.Math.Angle.Random();

         // set ball linear velocity according to angle and speed
         this.planckBody.setLinearVelocity(new planck.Vec2(this.speed * Math.cos(angle), this.speed * Math.sin(angle)))
    }

    // method to check if an edge is the one we already collided with
    sameEdgeCollision(edge : planck.Edge) : boolean {

        // check if current edge is the same as last collided edge
        let result : boolean = this.lastCollidedEdge == edge;

        // update last collided edge
        this.lastCollidedEdge = edge;

        // return the result
        return result;   
    }

    // method to handle bounce increasing speed number
    handleBounce(angle : number, speedIncrease : number) : void {

        // increase speed
        this.speed += speedIncrease;

        // set linear velocity according to angle and speed
        this.planckBody.setLinearVelocity(new planck.Vec2(this.speed * Math.cos(angle), this.speed * Math.sin(angle)));
    }

    // method to determine bounce angle against a line
    determineBounceAngle(line : Phaser.Geom.Line) : void {

        // get linear velocity
        let velocity : planck.Vec2 = this.planckBody.getLinearVelocity();

        // transform linear velocity into a line
        let ballLine : Phaser.Geom.Line = new Phaser.Geom.Line(0, 0, velocity.x, velocity.y);

        // calculate reflection angle between two lines
        this.angleToReflect = Phaser.Geom.Line.ReflectAngle(ballLine, line);   
    }

    // method to update ball position
    updatePosition() : void {
        
        // get ball planck body position
        let ballBodyPosition : planck.Vec2 = this.planckBody.getPosition();  

        // update ball position
        this.setPosition(toPixels(ballBodyPosition.x), toPixels(ballBodyPosition.y));  
    }
}

planckCompound.ts

Custom class to define the planck compound object.

import * as planck from 'planck';
import { toMeters, toPixels } from './planckUtils';
 
export class PlanckCompound {

    // planck body
    planckBody : planck.Body;

    // flag to check if the compound is active
    active : boolean;

    // sprites to render the compound
    sprites : Phaser.GameObjects.Sprite[];
 
    constructor(scene : Phaser.Scene, world : planck.World, posX : number, posY : number, offset : number, tickness : number) {
 
        // this is how we create a generic Box2D body
        let compound : planck.Body = world.createKinematicBody();

        // set body position
        compound.setPosition(new planck.Vec2(toMeters(posX), toMeters(posY)))
        
        // add edge fixtures to body
        compound.createFixture(planck.Edge(new planck.Vec2(toMeters(0), toMeters(-offset)), new planck.Vec2(toMeters(offset), toMeters(0))));
        compound.createFixture(planck.Edge(new planck.Vec2(toMeters(offset), toMeters(0)), new planck.Vec2(toMeters(0), toMeters(offset))));
        compound.createFixture(planck.Edge(new planck.Vec2(toMeters(0), toMeters(offset)), new planck.Vec2(toMeters(-offset), toMeters(0))));
        compound.createFixture(planck.Edge(new planck.Vec2(toMeters(-offset), toMeters(0)), new planck.Vec2(toMeters(0), toMeters(-offset))));

        // give the compound object a random angle
        compound.setAngle(Phaser.Math.Angle.Random());

        // give the compound object an angular velocity, to make it rotate a bit
        compound.setAngularVelocity(0.1);

        // assign the body to planckBody property
        this.planckBody = compound;

        // calculate edge length with Pythagorean theorem
        let edgeLength : number = Math.sqrt(offset * offset + offset * offset);

        // create sprites to render the compound
        this.sprites = [
            scene.add.sprite(0, 0, 'wall'),
            scene.add.sprite(0, 0, 'wall'),
            scene.add.sprite(0, 0, 'wall'),
            scene.add.sprite(0, 0, 'wall')
        ]

        // loop through all sprites
        this.sprites.forEach((sprite : Phaser.GameObjects.Sprite) => {

            // adjust display size
            sprite.setDisplaySize(edgeLength + tickness * 2, tickness);

            // set sprite origin to horizontal center, vertical bottom
            sprite.setOrigin(0.5, 1);

            // set sprite to semi transparent
            sprite.setAlpha(0.3);
        });

        // the compound is not active at the beginning
        this.active = false;
    }

    // method to set the compound walls to active or non active
    setActive(isActive : boolean) : void {
        
        // loop through all sprites
        this.sprites.forEach((sprite : Phaser.GameObjects.Sprite) => {

            // set sprite to semi transparent if the compound is not active, fully opaque otherwise
            sprite.setAlpha(isActive ? 1 : 0.3);
        });
        
        // set acrive property according to method argument 
        this.active = isActive;
    }

    // method to draw compount walls in a Graphics game object
    drawWalls(graphics : Phaser.GameObjects.Graphics) : void {

        // clear the graphics
        graphics.clear();

        // set line style to one pixel black
        graphics.lineStyle(1, 0x000000);

        // just a counter to see how many fixtures we processed so far
        let fixtureIndex : number = 0;

        // loop through all body fixtures
        for (let fixture : planck.Fixture = this.planckBody.getFixtureList() as planck.Fixture; fixture; fixture = fixture.getNext() as planck.Fixture) {
            
            // get fixture edge
            let edge : planck.Edge = fixture.getShape() as planck.Edge;

            // get edge vertices
            let lineStart : planck.Vec2 = this.planckBody.getWorldPoint(edge.m_vertex1);
            let lineEnd : planck.Vec2 = this.planckBody.getWorldPoint(edge.m_vertex2);
            
            // turn the planck edge into a Phaser line
            let drawLine : Phaser.Geom.Line = new Phaser.Geom.Line(
                toPixels(lineStart.x),
                toPixels(lineStart.y),
                toPixels(lineEnd.x),
                toPixels(lineEnd.y)
            );

            // get line mid point
            let lineMidPoint : Phaser.Geom.Point = Phaser.Geom.Line.GetMidPoint(drawLine);
            
            // get line angle
            let lineAngle : number = Phaser.Geom.Line.Angle(drawLine);

            // stroke the line shape
            graphics.strokeLineShape(drawLine);

            // place the sprite in the middle of the edge
            this.sprites[fixtureIndex].setPosition(lineMidPoint.x, lineMidPoint.y);

            // rotate the sprite according to edge angle
            this.sprites[fixtureIndex].setRotation(lineAngle);

            // increase the counter processed texture
            fixtureIndex ++;
        }  
    }
}

Now the prototype is playable. We should add bonus time when players activate the walls just a few milliseconds the ball is lost, it could be interesting. I’ll show you how to do it next time, meanwhile download the source code.

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