DROP’d HTML5 game prototype powered by Phaser ported to TypeScript – multiple cameras and commented source code

Read all posts about "" game

The process of conversion from JavaScript to TypeScript continues. I already built Perfect Square! and a DROP’d prototype using TypeScript, but both source codes were uncommented.

Now, I commented line by line the DROP’d prototype, and I also added some new features, like multiple cameras, tweens and time events, and now following the source code you will learn, among other things, how to use:

* Arcade Physics simulation with gravity, velocity and collisions.
* Input listeners
* Tweens
* Time Events
* Sprites
* Groups
* Render Textures
* Particle explosions
* Multiple cameras

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.

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, the one called by index.html.
Here we import most of the game libraries and define both Scale Manager object and Physics object.

Here we also initialize the game itself.

// MAIN GAME FILE

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

// 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: 1334
}

// obhect to initialize Arcade physics
const physicsObject: Phaser.Types.Core.PhysicsConfig = {
    default: 'arcade',
    arcade: {
        gravity: {
            y: GameOptions.gameGravity
        }
    }    
}

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

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

preloadAssets.ts

Class to preload all assets used in the game.

// CLASS TO PRELOAD ASSETS

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

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

    // preloading assets, the good old way
    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');
        this.load.image('sky', 'assets/sky.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.

// CONFIGURABLE GAME OPTIONS

export const GameOptions = {

    // first platform position, in height ratio. In this case, in the top 2/10 of the screen
    firstPlatformPosition: 2 / 10,
    
    // Arcade physics gravity
    gameGravity: 1700,

    // platform horizontal speed range, in pixels per second
    platformHorizontalSpeedRange: [250, 400],

    // platform width range, in pixels
    platformWidthRange: [120, 300],

    // platform vertical distance range, in pixels
    platformVerticalDistanceRange: [150, 250],

    // platform height, in pixels
    platformHeight: 50
}

playGame.ts

The game itself, the biggest class, game logic is stored here.

// THE GAME ITSELF

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

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

    // sprite with platform eyes
    eyes: Phaser.GameObjects.Sprite;

    // graphics where to draw platform borders
    borderGraphics: Phaser.GameObjects.Graphics;

    // tile sprite, used for platform background
    spritePattern: Phaser.GameObjects.TileSprite;

    // the hero!
    hero: PlayerSprite;
  
    // explosion emitter
    emitter: Phaser.GameObjects.Particles.ParticleEmitter;

    // particles to be emitted
    particles:  Phaser.GameObjects.Particles.ParticleEmitterManager;

    // group which will contain all platforms
    platformGroup: Phaser.Physics.Arcade.Group;

    // here we store game width once for all
    gameWidth: number;

    // here we store game height once for all
    gameHeight: number;

    // camera following game action
    actionCamera: Phaser.Cameras.Scene2D.Camera;

    // background sky
    sky: Phaser.GameObjects.Sprite;
   
    // constructor
    constructor() {
        super({
            key: 'PlayGame'
        });
    }

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

        // save game width value
        this.gameWidth = this.game.config.width as number;

        // save game height value
        this.gameHeight = this.game.config.height as number;
        
        // method to add the background sky
        this.addSky();

        // add a new Graphics object...
        this.borderGraphics = this.add.graphics();

        // ... and set it to invisible
        this.borderGraphics.setVisible(false);

        // add a new tile sprite...
        this.spritePattern = this.add.tileSprite(this.gameWidth / 2, GameOptions.platformHeight / 2,this.gameWidth, GameOptions.platformHeight * 2, 'pattern');
        
        // ... and set it to invisible
        this.spritePattern.setVisible(false);

        // add a new sprite...
        this.eyes = this.add.sprite(0, 0, 'eyes');

        // ... and set it to invisible
        this.eyes.setVisible(false);
        
        // create a new physics group
        this.platformGroup = this.physics.add.group();
        
        // we are going to add 12 platforms, and reuse them when we need them
        for (let i: number = 0; i < 12; i ++) {

            // method to add a platform
            this.addPlatform(i == 0);
        }

        // add the hero
        this.hero = new PlayerSprite(this, this.gameWidth / 2, 0, 'hero');
	    
        // input listener
        this.input.on('pointerdown', this.destroyPlatform, this);
        
        // method to add the emitter
        this.createEmitter();
     
        // method to set the cameras
        this.setCameras();
    }

    // method to add the sky
    addSky(): void {

        // add sky sprite
        this.sky = this.add.sprite(0, 0, 'sky');

        // adjust sprite size to cover the entire screen
        this.sky.displayWidth = this.gameWidth;
        this.sky.displayHeight = this.gameHeight;

        // set sprite origin to top left corner
        this.sky.setOrigin(0, 0);
    }

    // method to set the cameras
    setCameras(): void {

        // add a new camera
        this.actionCamera = this.cameras.add(0, 0, this.gameWidth, this.gameHeight);

        // action camera won't render the sky
        this.actionCamera.ignore([this.sky]);

        // make action camera follow the hero
        this.actionCamera.startFollow(this.hero, true, 0, 0.5, 0, - (this.gameHeight / 2 - this.gameHeight * GameOptions.firstPlatformPosition));

        // main camera won't render, particles, hero and platforms
        this.cameras.main.ignore([this.particles]);
        this.cameras.main.ignore([this.hero]);
        this.cameras.main.ignore(this.platformGroup);
    }

    // method to crate the emitter
    createEmitter(): void {

        // create new particles
        this.particles = this.add.particles('particle');
        
        // create the emitter
        this.emitter = this.particles.createEmitter({
            
            // particles will scale from 1 (original size) to zero (too tiny to be seen)
            scale: {
                start: 1,
                end: 0
            },

            // speed will scale from zero to 200 pixels per second
            speed: {
                min: 0,
                max: 200
            },

            // emitter is not active at the moment
            active: false,

            // particle lifespan, in milliseconds
            lifespan: 500,

            // quantity of particles to be used
            quantity: 50
        });
    }

    // method to add a platform 
    addPlatform(isFirst: Boolean): void {
        
        // creation of a new platform sprite
        let platform: PlatformSprite = new PlatformSprite(this, this.gameWidth / 2, isFirst ? this.gameWidth * GameOptions.firstPlatformPosition : 0, this.gameWidth / 8, GameOptions.platformHeight);
        
        // add platform to platformGroup group
        this.platformGroup.add(platform);

        // call platform setPhysics method
        platform.setPhysics();

        // call platform drawTexture method
        platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);

        // is this the first platform?
        if (isFirst) {

            // paint it green
            platform.setTint(0x00ff00);
        }
        else {

            // initialize the platform
            this.initPlatform(platform);
        }
    }

    // method to destroy a platform
    destroyPlatform(): void {

        // can the hero destroy the platform and is the hero alive?
        if (this.hero.canDestroyPlatform && !this.hero.isDying) {

            // hero can't destroy the platform anymore
            this.hero.canDestroyPlatform = false;

            // get the platform body which is the closest to the hero
            let closestPlatform: Phaser.Physics.Arcade.Body = this.physics.closest(this.hero) as Phaser.Physics.Arcade.Body;

            // given the body, get the sprite
            let platform: PlatformSprite = closestPlatform.gameObject as PlatformSprite;

            // make platform explode
            platform.explodeAndDestroy(this.emitter);

            // initialize the platform again
            this.initPlatform(platform);
        }
    }

    // method to initialize the platform
    initPlatform(platform: PlatformSprite): void {

        // set assignedVelocity property a random value
        platform.assignedVelocity = this.randomValue(GameOptions.platformHorizontalSpeedRange) * Phaser.Math.RND.sign();
        
        // change platform size
        platform.transformTo(this.gameWidth / 2, this.getLowestPlatform() + this.randomValue(GameOptions.platformVerticalDistanceRange), this.randomValue(GameOptions.platformWidthRange), GameOptions.platformHeight);
        
        // draw a new texture on the platform
        platform.drawTexture(this.borderGraphics, this.spritePattern, this.eyes);
    }

    // method to get the lowest platform position
    getLowestPlatform(): number {

        // set lowestplatform to zero
        let lowestPlatform: number = 0;

        // get all platforms
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];
        
        // iterate through all platforms
        for (let platform of platforms) {

            // lowestplatform is the max value between itself and platform vertical position
            lowestPlatform = Math.max(lowestPlatform, platform.y);    
        }

        // return lowestPlatform value
        return lowestPlatform;
    }

    // method to get the highest platform
    getHighestPlatform(maxHeight: number): PlatformSprite {

        // set highest platform y position to Infinity
        let highestPlatformY = Infinity;

        // get all platforms
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];

        // set an arbitrary platform as highest platform
        let highestPlatform: PlatformSprite = platforms[0];

        // iterate through all platforms
        for (let platform of platforms) {

            // is platform y position is greater than maxHeight and less than highest platform y?
            if (platform.y > maxHeight && platform.y < highestPlatformY) {

                // ... highestPlatform is now the current platform
                highestPlatform = platform;

                // update highest platform y
                highestPlatformY = platform.y;
            }   
        }

        // return highestPlatform
        return highestPlatform;
    }

    // method to toss a random value between two elements in an array
    randomValue(a: number[]): number {
        return Phaser.Math.Between(a[0], a[1]);
    }
    
    // method to check for collision between two bodies
    handleCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {

        // first body is the hero
        let hero: PlayerSprite = body1 as PlayerSprite;

        // second body is the platform
        let platform: PlatformSprite = body2 as PlatformSprite;

        // if the hero is not on the platform...
        if (!platform.isHeroOnIt) {

            // is the hero too close to left platform edges or landing on the left side of an illegal platform?
            if (hero.x < platform.getBounds().left || (!platform.isTinted && hero.x <= platform.x)) {

                // make hero fall down and die to the left
                this.fallAndDie(-1);

                // exit the function
                return; 
            }

            // is the hero too close to right platform edges or landing on the right side of an illegal platform?
            if (hero.x > platform.getBounds().right || (!platform.isTinted && hero.x > platform.x)) {

                 // make hero fall down and die to the right
                this.fallAndDie(1);

                // exit the function
                return; 
            }

            // now this platform has the hero on it
            platform.isHeroOnIt = true;

            // method to paint safe platforms
            this.paintSafePlatforms();

            // hero can destroy platforms once more
            this.hero.canDestroyPlatform = true;
        }
    }

    // method to make the hero fall and die
    fallAndDie(mult: number): void {

        // call hero's die method
        this.hero.die(mult);

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

            // after 800 milliseconds...
            delay: 800,
            callbackScope: this,
            callback: function() {

                // make action camera to stop following the hero
                this.actionCamera.stopFollow();
            }
        });
    }

    // method to paint safe platforms
    paintSafePlatforms(): void {

        // set floor platform as the highest platform
        let floorPlatform: PlatformSprite = this.getHighestPlatform(0);

        // tint floor platform red
        floorPlatform.setTint(0xff0000);

        // set target platform as the highest platform, but below floor platform
        let targetPlatform: PlatformSprite = this.getHighestPlatform(floorPlatform.y);

        // tint target platform green
        targetPlatform.setTint(0x00ff00);

        // player can land on this platform
        targetPlatform.canLandOnIt = true;
    }

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

        // is hero alive?
        if (!this.hero.isDying) {

            // manage collisions between the hero and platforms
            this.physics.world.collide(this.hero, this.platformGroup, this.handleCollision, undefined, this);
        }

        // get all platforms
        let platforms: PlatformSprite[] = this.platformGroup.getChildren() as PlatformSprite[];

        // iterate through all platforms
        for (let platform of platforms) {

            // if platform y position plus game height is less than hero y position...
            if (platform.y + this.gameHeight < this.hero.y) {

                // game over, let's start again
                this.scene.start('PlayGame');    
            }   

            // check distance between the platform and game edges
            let distance: number = Math.max(0.2, 1 - ((Math.abs(this.gameWidth / 2 - platform.x) / (this.gameWidth / 2)))) * Math.PI / 2;

            // assign platform velocity
            platform.body.setVelocityX(platform.assignedVelocity * distance);

            // if a platform reached the left or right edge...
            if ((platform.body.velocity.x < 0 && platform.getBounds().left < this.hero.displayWidth / 2) || (platform.body.velocity.x > 0 && platform.getBounds().right > this.gameWidth - this.hero.displayWidth / 2)) {
                
                // ... invert its velocity
                platform.assignedVelocity *= -1;
           }
        }
    }
}

playerSprite.ts

Class to define the player Sprite.

// PLATFORM SPRITE CLASS    

// player sprite extends Sprite class
export default class PlayerSprite extends Phaser.Physics.Arcade.Sprite {

    // can the player destroy the platform?
    canDestroyPlatform: Boolean = false;

    // is the hero dying?
    isDying: Boolean = false;

    // scene which called this class
    mainScene: Phaser.Scene;

    // constructor
	constructor(scene: Phaser.Scene, x: number, y: number, key: string) {
		super(scene, x, y, key);

        // add the player to the scnee
        scene.add.existing(this);

        // add physics body to platform
        scene.physics.add.existing(this);

        // save the scene which called this class
        this.mainScene = scene;
	}

    // method to make the player die
    die(mult: number): void {

        // hero is dying
        this.isDying = true;

        // set vertical velocity to -200 pixels per second (going up)
        this.setVelocityY(-200);

        // set horizontal velocity to 200 or -200 pixels per second
        this.setVelocityX(200 * mult);

        // set player angle to 45 or -45 using a 0.5s tween
        this.mainScene.tweens.add({
            targets: this,
            angle: 45 * mult,
            duration: 500
        });
    }
}

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.

// PLATFORM SPRITE CLASS    

// modules to import
import { GameOptions } from './gameOptions';

// platform sprite extends RenderTexture class
export default class PlatformSprite extends Phaser.GameObjects.RenderTexture {

    // does this platform have the player on it?
    isHeroOnIt: Boolean = false;

    // platform physics body
    body: Phaser.Physics.Arcade.Body;

    // platform assigned velocity
    assignedVelocity: number = 0;

    // can the player land on this platform?
    canLandOnIt: Boolean = false;

	// constructor
    constructor(scene: Phaser.Scene, x: number, y: number, width: number, height: number) {
		super(scene, x, y, width, height);
        
        // set platform origin in horizontal and vertical middle
        this.setOrigin(0.5);

        // add existing platform to scene
        scene.add.existing(this);

        // add a physics body to platform
        scene.physics.add.existing(this);
    }

    // method to set physics properties
    setPhysics(): void {

        // set the body immovable, it won't react to collisions 
        this.body.setImmovable(true);

        // do not allow gravity to move the body
        this.body.setAllowGravity(false);

        // set body friction to max (1), or player sprite will slip away
        this.body.setFrictionX(1);
    }

    // method to draw platform texture
    drawTexture(border: Phaser.GameObjects.Graphics, pattern: Phaser.GameObjects.TileSprite, eyes: Phaser.GameObjects.Sprite): void {
        
        // clear border graphics
        border.clear();

        // set border line style
        border.lineStyle(8, 0x000000, 1);

        // stroke a rectangle
        border.strokeRect(0, 0, this.displayWidth, this.displayHeight);

        // draw pattern texture on platform
        this.draw(pattern, this.displayWidth / 2, Phaser.Math.Between(0, GameOptions.platformHeight));

        // draw eyes texture on platform
        this.draw(eyes, this.displayWidth / 2, this.displayHeight / 2);

        // draw border graphics on platform
        this.draw(border);
    }

    // method to resize the platform
    transformTo(x: number, y: number, width: number, height: number): void {

        // change platform x position
        this.x = x;

        // change platform y position
        this.y = y;

        // set new platform size
        this.setSize(width, height);

        // set new Arcade body size
        this.body.setSize(width, height);
    }

    // method to explode and destroy the platform    
    explodeAndDestroy(emitter: Phaser.GameObjects.Particles.ParticleEmitter): void {

        // get platform bounds
        let platformBounds: Phaser.Geom.Rectangle = this.getBounds();

        // place emitter in top left vertex of the platform
        emitter.setPosition(platformBounds.left, platformBounds.top);

        // set the emitter as active
        emitter.active = true;

        // set emit zone to cover the entire platform
        emitter.setEmitZone({
            source: new Phaser.Geom.Rectangle(0, 0, platformBounds.width, platformBounds.height),
            type: 'random',
            quantity: 50
        });

        // make emitter explode
        emitter.explode(50, this.x - this.displayWidth / 2, this.y - this.displayHeight / 2);

        // remove platform tint
        this.clearTint();

        // player is no longer on this platform
        this.isHeroOnIt = false;

        // player can't land on this platform
        this.canLandOnIt = false;
    }
}

And that’s it TypeScript is not hard to learn, and thanks to Visual Studio Code IDE, development could be even faster and easier.

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

215 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
// Stairs
// 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