DROP’d HTML5 game prototype powered by Phaser ported to TypeScript – Build an endless game using Phaser and Arcade Physics

Read all posts about "" game

Here we go with another conversion from JavaScript to TypeScript, to learn coding the modern way.

Last week we saw Perfect Square! full game ported from JavaScript to TypeScript, now it’s time to see a prototype I started to code in January: DROP’d.

Look at the game we are going to build:

Tap to destroy your platform, and try to land on green platform. If you miss it, it’s game over.

The game features some nice tricks you can see explained in the tutorial series. The code which follows is the conversion from the latest JavaScript step.

If you don’t know how to configure your system to start conding and publishing with Phaser and TypeScript, I wrote a four steps guide about the migration from JavaScript to TypeScript, check steps 1, 2, 3 and 4.

To ensure maximum code reusability, the source code is split in 6 TypeScript file and one HTML file.

Let’s see them in detail:

index.html

The webpage which hosts the game, just the bare bones of HTML and main.ts is called.

<!DOCTYPE html>
<html>
    <head>
        <style type = "text/css">
            body {
                background: #000000;
                padding: 0px;
                margin: 0px;
            }
        </style>
        <script src = "scripts/main.ts"></script>
    </head>
    <body>
        <div id = "thegame"></div>
    </body>
</html>

main.ts

The main TypeScript file, we import most of the game and define both Scale Manager object and Physics object.

Here we also initialize the game itself.

import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame} from './playGame';
import { GameOptions } from './gameOptions';

const scaleObject: Phaser.Types.Core.ScaleConfig = {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH,
    parent: 'thegame',
    width: 750,
    height: 1334
}

const physicsObject: Phaser.Types.Core.PhysicsConfig = {
    default: 'arcade',
    arcade: {
        gravity: {
            y: GameOptions.gameGravity
        }
    }    
}

const configObject: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    backgroundColor:0x87ceea,
    scale: scaleObject,
    scene: [PreloadAssets, PlayGame],
    physics: physicsObject
}

new Phaser.Game(configObject);

preloadAssets.ts

Class to preload all assets

export class PreloadAssets extends Phaser.Scene {
    
    constructor() {
        super({
            key: 'PreloadAssets'
        });
    }

    preload(): void {
        this.load.image('hero', 'assets/hero.png');
        this.load.image('pattern', 'assets/pattern.png');
        this.load.image('eyes', 'assets/eyes.png');
        this.load.image('particle', 'assets/particle.png');
    }

	create(): void {
        this.scene.start('PlayGame');
    }
}

gameOptions.ts

Game options which can be changed to tune the gameplay are stored in a separate module, ready to be reused.

export const GameOptions = {
    firstPlatformPosition: 2 / 10,
    gameGravity: 1700,
    platformHorizontalSpeedRange: [250, 400],
    platformLengthRange: [120, 300],
    platformVerticalDistanceRange: [150, 250],
    platformHeight: 50
}

playGame.ts

The game itself.

import { GameOptions } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';

export class PlayGame extends Phaser.Scene {

    eyes: Phaser.GameObjects.Sprite;
    hero: PlayerSprite;
    canDestroy: Boolean;
    emitter: Phaser.GameObjects.Particles.ParticleEmitter;
    platformGroup: Phaser.Physics.Arcade.Group;
    gameWidth: number;
    gameHeight: number;
    borderGraphics: Phaser.GameObjects.Graphics;
    spritePattern: Phaser.GameObjects.TileSprite;

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

    create(): void {
        this.gameWidth = this.game.config.width as number;
        this.gameHeight = this.game.config.height as number;
        this.borderGraphics = this.add.graphics();
        this.borderGraphics.setVisible(false);
        this.spritePattern = this.add.tileSprite(this.gameWidth / 2, GameOptions.platformHeight / 2,this.gameWidth, GameOptions.platformHeight * 2, 'pattern')
        this.spritePattern.setVisible(false);
        this.eyes = this.add.sprite(0, 0, 'eyes');
        this.eyes.setVisible(false);
        this.platformGroup = this.physics.add.group();
        for (let i: number = 0; i < 7; i ++) {
            this.addPlatform(i == 0);
        }
        this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, 'hero');
	    this.cameras.main.startFollow(this.hero, true, 0, 0.5, 0, - (this.gameHeight / 2 - this.gameHeight * GameOptions.firstPlatformPosition));
        this.input.on('pointerdown', this.destroyPlatform, this);
        this.createEmitter();
    }

    createEmitter(): void {
        this.emitter = this.add.particles('particle').createEmitter({
            scale: {
                start: 1,
                end: 0
            },
            speed: {
                min: 0,
                max: 200
            },
            active: false,
            lifespan: 500,
            quantity: 50
        });
    }

    addPlatform(isFirst: Boolean): void { 
        let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, isFirst ? this.gameWidth * GameOptions.firstPlatformPosition : 0, this.gameWidth / 8, GameOptions.platformHeight);
        this.platformGroup.add(platform);
        platform.setPhysics();
        platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
        if (!isFirst) {
            this.initPlatform(platform);
        }
        else {
            platform.setTint(0x00ff00);

        }
    }

    destroyPlatform(): void {
        if (this.canDestroy) {
            this.canDestroy = false;
            let closestPlatform: Phaser.Physics.Arcade.Body = this.physics.closest(this.hero) as Phaser.Physics.Arcade.Body;
            let platform: PlatformSprite = closestPlatform.gameObject as PlatformSprite;
            platform.explodeAndDestroy(this.emitter);
            this.initPlatform(platform);
        }
    }

    initPlatform(platform: PlatformSprite): void {
        platform.assignedVelocity = this.randomValue(GameOptions.platformHorizontalSpeedRange) * Phaser.Math.RND.sign();
        platform.transformTo(this.gameWidth / 2, this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange), this.randomValue(GameOptions.platformLengthRange), GameOptions.platformHeight);
        platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
    }

    getLowestPlatform(): number {
        let lowestPlatform: number = 0;
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
        for (let platform of platforms) {
            let y: number = platform.y;
            lowestPlatform = Math.max(lowestPlatform, y);    
        }
        return lowestPlatform;
    }

    getHighestPlatform(maxHeight: number): PlatformSprite {
        let highestPlatform: PlatformSprite = this.platformGroup.getFirst();
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
        for (let platform of platforms) {
            if ((platform.y > maxHeight) &amp;&amp; (!highestPlatform || platform.y < highestPlatform.y)) {
                highestPlatform = platform;
            }   
        }
        return highestPlatform;
    }

    randomValue(a: number[]): number {
        return Phaser.Math.Between(a[0], a[1]);
    }

    handleCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {
        let hero: PlayerSprite = body1 as PlayerSprite;
        let platform: PlatformSprite = body2 as PlatformSprite;
        if (!platform.isHeroOnIt) {
            if (!platform.isTinted) {
                this.scene.start('PlayGame')
            }
            if (hero.x < platform.getBounds().left) {
                hero.setVelocityY(-200);
                hero.setVelocityX(-200);
                hero.angle = -45;
            }
            if (hero.x > platform.getBounds().right) {
                hero.setVelocityY(-200);
                hero.setVelocityX(200);
                hero.angle = 45;
            }
            platform.isHeroOnIt = true;
            this.paintSafePlatforms();
            this.canDestroy = true;
        }
    }

    paintSafePlatforms(): void {
        let floorPlatform: PlatformSprite = this.getHighestPlatform(0);
        floorPlatform.setTint(0xff0000);
        let targetPlatform: PlatformSprite = this.getHighestPlatform(floorPlatform.y);
        targetPlatform.setTint(0x00ff00);
    }

    update(): void {
        if (this.hero.angle == 0) {
            this.physics.world.collide(this.hero, this.platformGroup, this.handleCollision, undefined, this);
        }
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
        for (let platform of platforms) {
            if (platform.y + this.gameHeight < this.hero.y) {
                this.scene.start('PlayGame');    
            }   
            let distance: number = Math.max(0.2, 1 - ((Math.abs(this.gameWidth / 2 - platform.x) / (this.gameWidth / 2)))) * Math.PI / 2;
            platform.body.setVelocityX(platform.assignedVelocity * distance);
            if ((platform.body.velocity.x < 0 &amp;&amp; platform.getBounds().left < this.hero.displayWidth / 2) || (platform.body.velocity.x > 0 &amp;&amp; platform.getBounds().right > this.gameWidth - this.hero.displayWidth / 2)) {
                platform.assignedVelocity *= -1;
           }
        }
    }
}

playerSprite.ts

Class to define the player Sprite.

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

    canDestroyPlatform: Boolean = false;

	constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
		super(scene, x, y, key);
        scene.add.existing(this);
        scene.physics.add.existing(this);
	}
}

platformSprite.ts

Class to define the platform Sprite, which is the main actor of the game. We don’t control the player, but we can contro platforms by destroying them.

Also, platforms are rendered using RenderTexture object.

import { GameOptions } from './gameOptions';

export default class PlatformSprite extends Phaser.GameObjects.RenderTexture {

    isHeroOnIt: Boolean = false;
    body: Phaser.Physics.Arcade.Body;
    assignedVelocity: number = 0;

	constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number) {
		super(scene, x, y, width, height);
        this.setOrigin(0.5);
        scene.add.existing(this);
        scene.physics.add.existing(this);
    }

    setPhysics(): void {
        this.body.setImmovable(true);
        this.body.setAllowGravity(false);
        this.body.setFrictionX(1);
    }

    drawTexture(border: Phaser.GameObjects.Graphics, pattern: Phaser.GameObjects.TileSprite, eyes: Phaser.GameObjects.Sprite): void {
        border.clear();
        border.lineStyle(8, 0x000000, 1);
        border.strokeRect(0, 0, this.displayWidth, this.displayHeight);
        this.draw(pattern, this.displayWidth / 2, Phaser.Math.Between(0, GameOptions.platformHeight));
        this.draw(eyes, this.displayWidth / 2, this.displayHeight / 2);
        this.draw(border);
    }

    transformTo(x: number, y: number, width: number, height: number): void {
        this.x = x;
        this.y = y;
        this.setSize(width, height);
        this.body.setSize(width, height);
    }

    explodeAndDestroy(emitter: Phaser.GameObjects.Particles.ParticleEmitter): void {
        let platformBounds: Phaser.Geom.Rectangle = this.getBounds();
        emitter.setPosition(platformBounds.left, platformBounds.top);
        emitter.active = true;
        emitter.setEmitZone({
            source: new Phaser.Geom.Rectangle(0, 0, platformBounds.width, platformBounds.height),
            type: 'random',
            quantity: 50
        });
        emitter.explode(50, this.x - this.displayWidth / 2, this.y - this.displayHeight / 2);
        this.clearTint();
        this.isHeroOnIt = false;
    }
}

And the second porting of a JavaScript prototype to TypeScript has been made.

Now I am going to finish the development of this game, adding comments and features, 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