“Block it” HTML5 game ported to TypeScript with some new features, powered by Phaser and Arcade physics

Read all posts about "" game

I already blogged about Block it and I want to talk one more time about this game for two reasons:

1 – I need to port it into TypeScript for a future project.

2 – It’s a quick physics game which has been built with Arcade physics, but I am already rewriting it with no physics engines at all, for a better portability.

Anyway, the game is quite simple:

Tap or click to start and to activate the upper and lower walls at the right time, to make the ball bounce. If the ball flies off the screen, it’s game over.

Ball bounces randomly each time it touches the upper or lower walls.

And here is the source code, mostly uncommented but quite straightforward.

Anyway, the interesting stuff will come when I’ll drop the physics engine.

The game is split into one html file, one css file and six 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 = {

    // duration of the wall, in milliseconds
    wallDuration : 100,
 
    // ball start speed, in pixels/second
    ballStartSpeed : 500,
 
    // ball speed increase at each successful bounce, in pixels/second
    ballSpeedIncrease : 20,

    // random bounce angle range
    angleRange : 50,

    // angle needed for the ball to go up, in degrees
    upDirection : 90,

    // angle needed for the ball to go down, in degrees
    downDirection : 270,

    // ball radius, in pixels
    ballRadius : 25,

    // wall thickness, in pixels
    wallSize : 32,

    // wall padding from game canvas, in pixels
    wallPadding : 16
}

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 : 480,
    height : 640
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0xfe5430,
    scale : scaleObject,
    physics : {
        default : 'arcade'
    },
    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 walls.

// 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');
	}
}

playGame.ts

Main game file, all game logic is stored here.

// THE GAME ITSELF

import { GameOptions } from './gameOptions';
import WallSprite from './wallSprite';
import BallSprite from './ballSprite';

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

    gameOver : boolean;
    canActivateWall : boolean;
    ballSpeed : number;
    wallGroup : Phaser.Physics.Arcade.Group;
    theBall : BallSprite;
    lowerWall : WallSprite;
    upperWall : WallSprite;
    gameWidth : number;
    gameHeight : number;
    gameStarted : boolean;
    score : number;
    scoreText : Phaser.GameObjects.Text;

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

    // method to be executed when the scene has been created
    create() : void {  
        this.score = 0;
        this.gameWidth = this.game.config.width as number;
        this.gameHeight = this.game.config.height as number;
        this.scoreText = this.add.text(this.gameWidth / 2, this.gameHeight / 2, '0', {
            fontFamily: 'Arial',
            fontStyle : 'bold',
            fontSize: '256px',
            color: '#ffffff'
        });
        this.scoreText.setOrigin(0.5);
        this.scoreText.setAlpha(0.3);
        this.gameStarted = false;
        this.gameOver = false;
        this.canActivateWall = true;
        this.ballSpeed = GameOptions.ballStartSpeed;
        this.wallGroup = this.physics.add.group();
        this.theBall = new BallSprite(this, this.gameWidth / 2, this.gameHeight / 2, GameOptions.ballRadius);
        let wallWidth : number = this.gameWidth - GameOptions.wallPadding * 2 - GameOptions.wallSize * 2;
        let wallHeight : number = this.gameHeight - GameOptions.wallPadding * 2;
        let totalSpacing : number = GameOptions.wallPadding + GameOptions.wallSize;
        new WallSprite(this, GameOptions.wallPadding, GameOptions.wallPadding, GameOptions.wallSize, wallHeight, this.wallGroup, false);
        new WallSprite(this, this.gameWidth - totalSpacing, GameOptions.wallPadding, GameOptions.wallSize, wallHeight, this.wallGroup, false);
        this.upperWall = new WallSprite(this, totalSpacing, GameOptions.wallPadding, wallWidth, GameOptions.wallSize, this.wallGroup, true);
        this.lowerWall = new WallSprite(this, totalSpacing, this.gameHeight - totalSpacing, wallWidth, GameOptions.wallSize, this.wallGroup, true);
        this.physics.add.collider(this.theBall, this.wallGroup, this.ballVsWall, undefined, this);
        this.input.on('pointerdown', this.activateWall, this);   
    }

    // method to check collision between ball and walls
    ballVsWall(theBall : Phaser.Types.Physics.Arcade.GameObjectWithBody, theWall : Phaser.Types.Physics.Arcade.GameObjectWithBody) : void {
        let wall : WallSprite = theWall as WallSprite; 
        this.canActivateWall = true;
        if (wall.activable) {
            let direction : number = theBall.body.y < wall.body.y ? GameOptions.downDirection : GameOptions.upDirection;
            this.score ++;
            this.scoreText.setText(this.score.toString());
            this.randomBounce(direction);
        }
    }

    // method to activate the walls
    activateWall() : void {
        if (!this.gameStarted) {
            let direction : number = Phaser.Math.Between(0, 1) == 0 ? GameOptions.upDirection : GameOptions.downDirection;
            this.randomBounce(direction);
            this.upperWall.turnOff();
            this.lowerWall.turnOff();
            this.gameStarted = true;
            return;
        }
        if (this.canActivateWall) {
            this.canActivateWall = false;
            this.upperWall.turnOn();
            this.lowerWall.turnOn();
            this.time.addEvent({
                delay: GameOptions.wallDuration,
                callbackScope: this,
                callback: () => {
                    this.upperWall.turnOff();
                    this.lowerWall.turnOff();
                }
            });
        }
    }

    // method to give the ball a random bounce
    randomBounce(direction : number) : void {
        this.ballSpeed += GameOptions.ballSpeedIncrease;
        let ballVelocity : Phaser.Math.Vector2 = this.physics.velocityFromAngle(Phaser.Math.Between(direction - GameOptions.angleRange, direction + GameOptions.angleRange), this.ballSpeed);
        this.theBall.setVelocity(ballVelocity.x, ballVelocity.y);
    }

    // method to be called at each frame
    update() : void{
        if ((this.theBall.y > this.gameHeight || this.theBall.y < 0) &amp;&amp; !this.gameOver) {
            this.gameOver = true;
            this.cameras.main.shake(800, 0.05);
            this.time.addEvent({
                delay: 800,
                callbackScope: this,
                callback: () => {
                    this.scene.start('PlayGame');
                }
            });
        }
    }
}

wallSprite.ts

Custom class for the walls, extending Phaser.Physics.Arcade.Sprite.

export default class WallSprite extends Phaser.Physics.Arcade.Sprite {

    activable : boolean;

    constructor(scene : Phaser.Scene, posX : number, posY : number, width : number, height : number, group : Phaser.Physics.Arcade.Group, isActivable : boolean) {
        super(scene, posX, posY, 'wall');
        this.setDisplaySize(width, height);
        this.setOrigin(0);
        scene.add.existing(this);
        scene.physics.add.existing(this);
        group.add(this);
        this.setImmovable(true);
        this.activable = isActivable
    }

    turnOff() : void {
        this.setAlpha(0.1);
        this.body.checkCollision.none = true;
    }

    turnOn() : void {
        this.setAlpha(1);
        this.body.checkCollision.none = false;   
    }
}

ballSprite.ts

Custom class for the ball, extending Phaser.Physics.Arcade.Sprite.

export default class ballSprite extends Phaser.Physics.Arcade.Sprite {

    constructor(scene : Phaser.Scene, posX : number, posY : number, radius : number) {
        super(scene, posX, posY, 'ball');
        scene.add.existing(this);
        scene.physics.add.existing(this);
        this.body.setCircle(64);
        let ballSize : number = radius * 2
        this.setDisplaySize(ballSize, ballSize);
        this.setBounce(1);
    }
}

And that’s it. Wonder how to build a game like this with no physics engine, maybe featuring continuous collision detection? Stay tuned, and meanwhile download the source code.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

214 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers