“Serious Scramblers” HTML5 prototype built with Phaser and TypeScript – Adding a new enemy type: the Rotating Saw

Read all posts about "" game

As I foretold yesterday on my Twitter account (follow me if you didn’t already, to stay up to date), I updated my HTML5 Serious Scramblers prototype, adding a new enemy: the Rotating Saw.

Unlike angry pigs, saws can’t be killed by stomping on them.

Adding rotating saws was not that different than adding angry pigs. Actually it was even easier because I did not have to manage the case of the saw to be killed by the player.

Rotating saws too are managed by object pooling, but first of all let’s have a look at the game:

Tap and hold left or right to move the character left or right. Once you move, platforms will scroll up. Reach the top of the stage, and it’s game over.

Fall from platform to platform without falling too down, if you reach the bottom of the stage, it’s game over.

Touch an enemy, and it’s game over. But you can kill angry pigs by jumping on their head.

The game is made of 12 TypeScript files and one HTML file used in this prototype. Although I made some changes here and there since previous step, I am highlighting the most interesting pieces of code I added in this step.

index.html

The webpage which hosts the game, just the bare bones of HTML and main.ts is called. No changes have been made.

Also look at the thegame div, this is where the game will run.

<!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. No changes have been made

// 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: GameOptions.gameSize.width,
    height: GameOptions.gameSize.height
}

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

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

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

preloadAssets.ts

Class to preload all assets used in the game. This time we also load the rotating saw sprite sheet.

// CLASS TO PRELOAD ASSETS

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

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

    // preload assets
    preload(): void {
        this.load.image('platform', 'assets/platform.png');
        this.load.image('background', 'assets/background.png');
        this.load.image('leftplatformedge', 'assets/leftplatformedge.png');
        this.load.image('rightplatformedge', 'assets/rightplatformedge.png');
        this.load.spritesheet('enemy', 'assets/enemy.png', {
            frameWidth: 36,
            frameHeight: 30
        });
        this.load.spritesheet('enemy_hit', 'assets/enemy_hit.png', {
            frameWidth: 36,
            frameHeight: 30
        });
        this.load.spritesheet('hero', 'assets/hero.png', {
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.spritesheet('hero_run', 'assets/hero_run.png', {
            frameWidth: 32,
            frameHeight: 32
        });
        this.load.spritesheet('saw', 'assets/saw.png', {
            frameWidth: 38,
            frameHeight: 38
        });        
	}

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

        // call PlayGame class
        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.

I added some constants to store values, then changed the way we decide if we have to place an enemy on a platform.

An array called platformStuff is filled with enemy values, then each time a platform is initialized, we pick a random array item.

// CONFIGURABLE GAME OPTIONS

// some constants to store values
export const EMPTY_PLATFORM = 0;
export const ENEMY = 1;
export const SAW = 2;

export const GameOptions = {

    // game size, in pixels
    gameSize: {
        width: 750,
        height: 1334
    },

    // game scale ratio
    pixelScale: 3,

    // first platform vertical position. 0 = top of the screen, 1 = bottom of the screen
    firstPlatformPosition: 4 / 10,

    // game gravity, which only affects the hero
    gameGravity: 1200,

    // hero speed, in pixels per second
    heroSpeed: 300,

    // platform speed, in pixels per second
    platformSpeed: 90,

    // platform length range, in pixels
    platformLengthRange: [150, 250],

    // platform horizontal distance range from the center of the stage, in pixels
    platformHorizontalDistanceRange: [0, 250],

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

    // platform tint colors
    platformColors: [0xffffff, 0xff0000, 0x00ff00],

    // bounce velocity when landing on bouncing platform
    bounceVelocity: 500,

    // disappearing platform time before disappearing, in milliseconds
    disappearTime: 1000,

    // enemy patrolling speed range, in pixels per second
    enemyPatrolSpeedRange: [40, 80],

    // saw patrolling speed range, in pixels per second
    sawSpeedRange: [10, 30],

    // array filled with stuff to be added to the platform. An item is randomly picked each time we need to place some stuff on the platform
    platformStuff: [EMPTY_PLATFORM, ENEMY, ENEMY, ENEMY, ENEMY, SAW, SAW, SAW]
}

playGame.ts

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

In this file I added saw creation, management and collisions. It’s not that different than angry pigs management, anyway. It’s even simpler because saws can’t be killed.

// THE GAME ITSELF

// modules to import
import { GameOptions, ENEMY, SAW } from './gameOptions';
import PlayerSprite from './playerSprite';
import PlatformSprite from './platformSprite';
import EnemySprite from './enemySprite';
import PlatformGroup from './platformGroup';
import EnemyGroup from './enemyGroup';
import SawSprite from './sawSprite';
import SawGroup from './sawGroup';

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

    // group to contain all platforms
    platformGroup: PlatformGroup;

    // group to contain all enemies
    enemyGroup: EnemyGroup;

    // group to contain all saws
    sawGroup: SawGroup;

    // the hero of the game
    hero: PlayerSprite;

    // is it the first time player is moving?
    firstMove: Boolean;

    // enemy pool, built as an array
    enemyPool: EnemySprite[];

    // saw pool, built as an array
    sawPool: SawSprite[];

    // just a debug text to print some info
    debugText: Phaser.GameObjects.Text;

    // background image
    backgroundImage: Phaser.GameObjects.TileSprite;

    // left edge platform sprite
    leftPlatform: Phaser.GameObjects.Sprite;

    // right edge platform sprite
    rightPlatform: Phaser.GameObjects.Sprite;

    // middle platform sprite
    middlePlatform: Phaser.GameObjects.Sprite;

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

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

        // method to initialize platform sprites
        this.initializePlatformSprites();

        // method to inizialize animations
        this.initializeAnimations();

        // method to place the background image
        this.setBackground();   

        // add the debug text to the game
        this.debugText = this.add.text(16, 16, '', {
            color: '#000000',
            fontFamily: 'monospace',
            fontSize: '48px'
        });

        // initialize enemy pool as an empty array
        this.enemyPool = [];

        // initialize saw pool as an empty array
        this.sawPool = [];

        // this is the firt move
        this.firstMove = true;

        // create a new physics group for the platforms
        this.platformGroup = new PlatformGroup(this.physics.world, this);

        // create a new physics group for the enemies
        this.enemyGroup = new EnemyGroup(this.physics.world, this);

        // create a new physics group for the saws
        this.sawGroup = new SawGroup(this.physics.world, this);

        // let's create ten platforms. They are more than enough
        for (let i: number = 0; i < 10; i ++) {

            // create a new platform
            let platform: PlatformSprite = new PlatformSprite(this, this.platformGroup, this.leftPlatform, this.middlePlatform, this.rightPlatform);  
            
            // if it's not the first platform...
            if (i > 0) {

                // place some stuff on it
                this.placeStuffOnPlatform(platform);
            }
        }

        // add the hero
        this.hero = new PlayerSprite(this);

        // input listener to move the hero
        this.input.on("pointerdown", this.moveHero, this);

        // input listener to stop the hero
        this.input.on("pointerup", this.stopHero, this);
    }

    // method to set background image
    setBackground(): void {

        // add a tileSprite
        this.backgroundImage = this.add.tileSprite(0, 0, GameOptions.gameSize.width / GameOptions.pixelScale, GameOptions.gameSize.height / GameOptions.pixelScale, 'background');
        
        // set background origin to top left corner
        this.backgroundImage.setOrigin(0, 0);

        // set background scale
        this.backgroundImage.scale = GameOptions.pixelScale;
    }

    // method to inizialize animations
    initializeAnimations(): void {

        // hero idle animation
        this.anims.create({
            key: "idle",
            frames: this.anims.generateFrameNumbers('hero', {
                start: 0,
                end: 10
            }),
            frameRate: 20,
            repeat: -1
        });

        // hero run animation
        this.anims.create({
            key: "run",
            frames: this.anims.generateFrameNumbers('hero_run', {
                start: 0,
                end: 11
            }),
            frameRate: 20,
            repeat: -1
        });

        // enemy run animation
        this.anims.create({
            key: "enemy_run",
            frames: this.anims.generateFrameNumbers('enemy', {
                start: 0,
                end: 11
            }),
            frameRate: 20,
            repeat: -1
        });

        // enemy falling animation
        this.anims.create({
            key: "enemy_falling",
            frames: this.anims.generateFrameNumbers('enemy_hit', {
                start: 0,
                end: 2
            }),
            frameRate: 20
        });

        // saw animation
        this.anims.create({
            key: "saw",
            frames: this.anims.generateFrameNumbers('saw', {
                start: 0,
                end: 7
            }),
            frameRate: 20,
            repeat: -1
        });
    }

    // method to inizialize platform sprites
    initializePlatformSprites(): void {

        // add left platform edge sprite
        this.leftPlatform = this.add.sprite(0, 0, 'leftplatformedge');

        // set registration point to top left corner
        this.leftPlatform.setOrigin(0, 0);

        // set sprite to invisible
        this.leftPlatform.setVisible(false);

        // add right platform edge sprite
        this.rightPlatform = this.add.sprite(0, 0, 'rightplatformedge');

        // set registration point to top right corner
        this.rightPlatform.setOrigin(1, 0);

        // set sprite to invisible
        this.rightPlatform.setVisible(false);

        // add middle platform sprite
        this.middlePlatform = this.add.sprite(0, 0, 'platform');

        // set registration point to center
        this.middlePlatform.setOrigin(0, 0);

        // set sprite to invisible
        this.middlePlatform.setVisible(false);
    }

    // method to place stuff on platform
    // argument: the platform
    placeStuffOnPlatform(platform: PlatformSprite): void {

        // pick a random item from platformStuff array
        let randomPick: number = Phaser.Utils.Array.GetRandom(GameOptions.platformStuff);

        // add stuff according to random pick
        switch (randomPick) {

            // enemy
            case ENEMY: { 
                
                // is the enemy pool empty?
                if (this.enemyPool.length == 0) {

                    // create a new enemy sprite
                    new EnemySprite(this, platform, this.enemyGroup);
                }

                // enemy pool is not empty
                else {

                    // retrieve an enemy from the enemy pool
                    let enemy: EnemySprite = this.enemyPool.shift() as EnemySprite;

                    // move the enemy from the pool to enemy group
                    enemy.poolToGroup(platform, this.enemyGroup);
                }
                break;
            }

            // saw
            case SAW: {

                // is the saw pool empty?
                if (this.sawPool.length == 0) {
                    
                    // create a new saw sprite
                    new SawSprite(this, platform, this.sawGroup);
                }

                // saw pool is not empty
                else {

                    // retrieve a saw from the saw pool
                    let saw: SawSprite = this.sawPool.shift() as SawSprite;

                    // move the saw from the pool to saw group
                    saw.poolToGroup(platform, this.sawGroup);

                }
                break;
            }
        }
    }

    // method to move the hero
    // argument: the input pointer
    moveHero(e: Phaser.Input.Pointer): void {

        // set hero movement according to input position
        this.hero.setMovement((e.x > GameOptions.gameSize.width / 2) ? this.hero.RIGHT : this.hero.LEFT);

        // is it the first move?
        if (this.firstMove) {

            // it's no longer the first move
            this.firstMove = false;

            // move platform group
            this.platformGroup.setVelocityY(-GameOptions.platformSpeed);

            // move saw group
            this.sawGroup.setVelocityY(-GameOptions.platformSpeed);
        }
    }

    // method to stop the hero
    stopHero(): void {

        // ... just stop the hero :)
        this.hero.setMovement(this.hero.STOP);
    }

    // method to handle collisions between hero and saws
    // arguments: the two colliding bodies
    handleSawCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {
        
        // restart the game
        this.scene.start("PlayGame");           
    }

    // method to handle collisions between hero and enemies
    // arguments: the two colliding bodies
    handleEnemyCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {

        // first body is the hero
        let hero: PlayerSprite = body1 as PlayerSprite;
 
        // second body is the enemy
        let enemy: EnemySprite = body2 as EnemySprite;

        // the following code will be executed only if the hero touches the enemy on its upper side (STOMP!)
        if (hero.body.touching.down && enemy.body.touching.up) {

            // move the enemy from enemy group to enemy pool
            enemy.groupToPool(this.enemyGroup, this.enemyPool);

            // play "enemy_falling" animation
            enemy.anims.play('enemy_falling', true);

            // flip the enemy vertically
            enemy.setFlipY(true);

            // make the hero bounce
            hero.setVelocityY(GameOptions.bounceVelocity * -1);
        }

        // hero touched an enemy without stomping it
        else {

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

    // method to handle collisions between hero and platforms
    // arguments: the two colliding bodies
    handlePlatformCollision(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;

        // the following code will be executed only if the hero touches the platform on its upper side
        if (hero.body.touching.down && platform.body.touching.up) {

            // different actions according to platform type
            switch (platform.platformType) {

                // breakable platform
                case 1:

                    // if the platform is not already fading out...
                    if (!platform.isFadingOut) {

                        // flag the platform as a fading out platform
                        platform.isFadingOut = true;

                        // add a tween to fade the platform out
                        this.tweens.add({
                            targets: platform,
                            alpha: 0,
                            ease: 'bounce',
                            duration: GameOptions.disappearTime,
                            callbackScope: this,
                            onComplete: function() {

                                // reset the platform
                                this.resetPlatform(platform);
                            }
                        });
                    }
                    break;
                
                // bouncy platform
                case 2:

                    // make the hero jump changing vertical velocity
                    hero.setVelocityY(GameOptions.bounceVelocity * -1);
                    break;
            }
        }
    }

    // method to reset a platform
    // argument: the platform
    resetPlatform(platform: PlatformSprite): void {
          
        // recycle the platform
           platform.initialize();

        // place stuff on platform
        this.placeStuffOnPlatform(platform);
    }

    // method to handle collisions between enemies and platforms
    // arguments: the two colliding bodies
    handleEnemyPlatformCollision(body1: Phaser.GameObjects.GameObject, body2: Phaser.GameObjects.GameObject): void {
 
        // first body is the enemy
        let enemy: EnemySprite = body1 as EnemySprite;
 
        // second body is the platform
        let platform: PlatformSprite = body2 as PlatformSprite;

        // set the platform to patrol
        enemy.platformToPatrol = platform;
    }

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

        // if the hero is already moving...
        if (!this.firstMove) {

            // scroll a bit the background texture
            this.backgroundImage.tilePositionY += 0.2;
        }

        // move the hero
        this.hero.move();       

        // handle collision between hero and platforms
        this.physics.world.collide(this.hero, this.platformGroup, this.handlePlatformCollision, undefined, this);

        // handle collision between enemies and platforms
        this.physics.world.collide(this.enemyGroup, this.platformGroup, this.handleEnemyPlatformCollision, undefined, this);

        // handle collisions between hero and enemies
        this.physics.world.collide(this.hero, this.enemyGroup, this.handleEnemyCollision, undefined, this);

        // handle collisions between hero and saws
        this.physics.world.collide(this.hero, this.sawGroup, this.handleSawCollision, undefined, this);

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

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

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

            // if a platform leaves the stage to the upper side...
            if (platformBounds.bottom < 0) {

                // reset the platform
                this.resetPlatform(platform);
            }
        }

        // get all saws
        let saws: SawSprite[] = this.sawGroup.getChildren() as SawSprite[];

        // loop through all saws
        for (let saw of saws) {   

            // make enemy patrol
            saw.patrol();

            let sawBounds: Phaser.Geom.Rectangle = saw.getBounds();

            // if a saw leaves the stage to the upper side...
            if (sawBounds.bottom < 0) {

                // move the saw from saw group to saw pool
                saw.groupToPool(this.sawGroup, this.sawPool);
            }
        }

        // get all enemies
        let enemies: EnemySprite[] = this.enemyGroup.getChildren() as EnemySprite[];

        // update debug text
        this.debugText.setText("Enemies: group " + enemies.length.toString() + ", pool: " + this.enemyPool.length.toString() + "\nSaws: group " + saws.length.toString() + ", pool: " + this.sawPool.length.toString());

        // loop through all enemies in enemyPool array
        for (let enemy of this.enemyPool) {

            // if the enemy falls down the stage...
            if (enemy.y > GameOptions.gameSize.height + 100) {

                // set enemy velocity to zero
                enemy.setVelocity(0 ,0);

                // do not let enemy to be affacted by gravity
                enemy.body.setAllowGravity(false);
            }
        }

        // loop through all enemies
        for (let enemy of enemies) {   

            // make enemy patrol
            enemy.patrol();

            // get enemy bounds
            let enemyBounds: Phaser.Geom.Rectangle = enemy.getBounds();

            // if the enemy leaves the screen...
            if (enemyBounds.bottom < 0) {

                // move enemy from enemy group to enemy pool
                enemy.groupToPool(this.enemyGroup, this.enemyPool);

                // do not show the enemy
                enemy.setVisible(false); 
            }
        }
        
        // if the hero falls down or leaves the stage from the top...
        if (this.hero.y > GameOptions.gameSize.height || this.hero.y < 0) {

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

playerSprite.ts

Class to define the player Sprite, the main actor of the game, the one players control.

Did not change.

// PLAYER SPRITE CLASS  

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

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

    // assign LEFT property a -1 value, just like if it were a constant
    LEFT: number = -1;

    // assign RIGHT property a 1 value, just like if it were a constant
    RIGHT: number = 1;

    // assign STOP property a 0 value, just like if it were a constant
    STOP: number = 0;

    // player current movment is STOP
    currentMovement: number = this.STOP;

    // constructor
    // argument: game scene
	constructor(scene: Phaser.Scene) {
		super(scene, GameOptions.gameSize.width / 2, GameOptions.gameSize.height * GameOptions.firstPlatformPosition - 100, 'hero');

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

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

        // set player scale
        this.scale = GameOptions.pixelScale;

        // shrink a bit player pyhsics body size to make the game forgive players a bit
        this.body.setSize(this.displayWidth / GameOptions.pixelScale * 0.6, this.displayHeight / GameOptions.pixelScale * 0.7, false);

        // set player physics body offset. This has to be done manually, no magic formula
        this.body.setOffset(7, 9);
	}

    // method to set player movement
    // argument: the new movement
    setMovement(n: number): void {

        // set currentMovement to n
        this.currentMovement = n;
    }

    // method to move the player
    move(): void {

        // set player horizontal velocity according to currentMovement value
        this.setVelocityX(GameOptions.heroSpeed * this.currentMovement);
        
        // various cases according to player movememt
        switch (this.currentMovement) {

            // player is moving left
            case this.LEFT:

                // flip sprite horizontally
                this.setFlipX(true);

                // play "run" animation
                this.anims.play('run', true);
                break;

            // player is moving right
            case this.RIGHT:

                // do not flip sprite horizontally
                this.setFlipX(false);

                // play "run" animation
                this.anims.play('run', true);
                break;

            // player is not moving
            case this.STOP:

                // play "idle" animation
                this.anims.play('idle', true);
                break;
        }
    }
}

platformSprite.ts

Class to define the platforms. Did not change.

// PLATFORM SPRITE CLASS    

// modules to import
import PlatformGroup from './platformGroup';
import { GameOptions } from './gameOptions';
import { randomValue } from './utils';

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

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

    // platform type
    platformType: number = 0;

    // is the platform fading out?
    isFadingOut: Boolean = false;

    // platform group
    platformGroup: PlatformGroup;

    // the three sprites forming the platform: left edge, middle, and right edge
    leftSprite: Phaser.GameObjects.Sprite;
    middleSprite: Phaser.GameObjects.Sprite;
    rightSprite: Phaser.GameObjects.Sprite;

    // constructor
    // arguments: the game scene, the platform group, sprite for left edge, sprite for the middle, sprite for the right edge
    constructor(scene: Phaser.Scene, group: PlatformGroup, leftSprite: Phaser.GameObjects.Sprite, middleSprite: Phaser.GameObjects.Sprite, rightSprite: Phaser.GameObjects.Sprite) {
       super(scene, 0, 0, 1, 16);

        // set left, middle and right sprites
        this.leftSprite = leftSprite;
        this.middleSprite = middleSprite;
        this.rightSprite = rightSprite;

        // RenderTexture object does not have default origin at 0.5, so we need to set it
        this.setOrigin(0.5);

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

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

        // add the platform to group
        group.add(this);

        // platform body does not react to collisions
        this.body.setImmovable(true);

        // platform body is not affected by gravity
        this.body.setAllowGravity(false);

        // save platform group
        this.platformGroup = group;

        // let's initialize the platform, with random position, size and so on
        this.initialize();

        // set platform scale
        this.scale = GameOptions.pixelScale;
    }

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

        // platform is not fading out
        this.isFadingOut = false;

        // platform alpha is set to fully opaque
        this.alpha = 1;

        // get lowest platform Y coordinate
        let lowestPlatformY: number = this.platformGroup.getLowestPlatformY();

        // is lowest platform Y coordinate zero? (this means there are no platforms yet)
        if (lowestPlatformY == 0) {

            // position the first platform
            this.y = GameOptions.gameSize.height * GameOptions.firstPlatformPosition;
            this.x = GameOptions.gameSize.width / 2;
        }
        else {

            // position the platform
            this.y = lowestPlatformY + randomValue(GameOptions.platformVerticalDistanceRange);
            this.x = GameOptions.gameSize.width / 2 + randomValue(GameOptions.platformHorizontalDistanceRange) * Phaser.Math.RND.sign();

            // set a random platform type
            this.platformType = 0//Phaser.Math.Between(0, 2);
        }

        // platform width
        let newWidth: number = randomValue(GameOptions.platformLengthRange) / GameOptions.pixelScale;
        
        // set platform size
        this.setSize(newWidth, 16);

        // set platform body size
        this.body.setSize(newWidth, 16);
        
        // set middle sprite display width equal to entire platform width
        this.middleSprite.displayWidth = newWidth;

        // draw middle sprite
        this.draw(this.middleSprite, 0, 0);

        // draw left edge sprite
        this.draw(this.leftSprite, 0, 0);

        // draw right edge sprite
        this.draw(this.rightSprite, newWidth, 0);
    }
}

platformGroup.ts

Class to define the Phaser Group, dedicated to the group which contains all platforms. Did not change.

// PLATFORM GROUP CLASS  

// modules to import
import PlatformSprite from "./platformSprite";

// platform group extends Arcade Group class
export default class PlatformGroup extends  Phaser.Physics.Arcade.Group {
    
    // constructor
    // arguments: the physics world, the game scene
    constructor(world: Phaser.Physics.Arcade.World, scene: Phaser.Scene) {
        super(world, scene);
    }

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

        // lowest platform value is initially set to zero
        let lowestPlatformY: number = 0;

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

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

            // get the highest value between lowestPlatform and platform y coordinate
            lowestPlatformY = Math.max(lowestPlatformY, platform.y);
        };

        // return lowest platform coordinate
        return lowestPlatformY;
    }
}

enemySprite.ts

The class to define the patrolling enemy. Did not change.

// ENEMY SPRITE CLASS    

// modules to import
import EnemyGroup from "./enemyGroup";
import PlatformSprite from "./platformSprite";
import { randomValue } from './utils';
import { GameOptions } from './gameOptions';

// enemy sprite extends Arcade Sprite class
export default class EnemySprite extends Phaser.Physics.Arcade.Sprite {

    // the platform where the enemy is patrolling
    platformToPatrol: PlatformSprite;

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

    // constructor
    // arguments: the game scene, the platform where the enemy is on, and enemy group
    constructor(scene: Phaser.Scene, platform: PlatformSprite, group: EnemyGroup) {
        super(scene, platform.x, platform.y - 100, 'enemy');

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

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

        // set enemy scale
        this.scale = GameOptions.pixelScale;

        // shrink a bit enemy pyhsics body size to make the game forgive players a bit
        this.body.setSize(this.displayWidth / GameOptions.pixelScale * 0.6, this.displayHeight / GameOptions.pixelScale * 0.75, true);

        // set enemy physics body offset. This has to be done manually, no magic formula
        this.body.setOffset(7, 7)

        // the enemy is patrolling the current platform
        this.platformToPatrol = platform;

        // add the enemy to the group
        group.add(this);

        // set enemy horizontal speed
        this.setVelocityX(randomValue(GameOptions.enemyPatrolSpeedRange) * Phaser.Math.RND.sign());

        // play "enemy_run" animation
        this.anims.play('enemy_run', true);
    }

    // method to make the enemy patrol a platform
    patrol(): void { 

        // flip enemy sprite if moving right
        this.setFlipX(this.body.velocity.x > 0);

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

        // get enemy bounds
        let enemyBounds: Phaser.Geom.Rectangle = this.getBounds();

        // get enemy horizontal speeds
        let enemyVelocityX: number = this.body.velocity.x
       
        // if the enemy is moving left and is about to fall down the platform to the left side
        // or the enemy is moving right and is about to fall down the platform to the right side
        if ((platformBounds.right + 25 < enemyBounds.right && enemyVelocityX > 0) || (platformBounds.left - 25 > enemyBounds.left && enemyVelocityX < 0)) {

            // invert enemy horizontal speed
            this.setVelocityX(enemyVelocityX * -1);
        }
    }

    // method to remove the enemy from a group and place it into the pool
    // arguments: the group and the pool
    groupToPool(group: EnemyGroup, pool: EnemySprite[]): void {

        // remove enemy from the group
        group.remove(this);

        // push the enemy in the pool
        pool.push(this);  
    }

    // method to remove the enemy from the pool and place it into a group
    // arguments: the platform to patrol and the group
    poolToGroup(platform: PlatformSprite, group: EnemyGroup): void {

        
        // set the platform to patrol
        this.platformToPatrol = platform;

        // place the enemy in the center of the platform
        this.x = platform.x;

        // place the enemy a little above the platform
        this.y = platform.y - 120;

        // set the enemy visible
        this.setVisible(true);

        // add the enemy to the group
        group.add(this);

        // allow gravity to affect the enemy
        this.body.setAllowGravity(true);

        // set enemy horizontal speed
        this.setVelocityX(randomValue(GameOptions.enemyPatrolSpeedRange) * Phaser.Math.RND.sign());

        // play "enemy_run" animation
        this.anims.play('enemy_run', true);

        // do not vertically flip enemy sprite
        this.setFlipY(false);
    }
}

enemyGroup.ts

Actually this class to extend the Phaser Group which contains all enemies does not add any custom feature, but I preferred to create a custom class like I did with platformGroup.ts. Did not change

// ENEMY GROUP CLASS  

// enemy group extends Arcade Group class
export default class EnemyGroup extends  Phaser.Physics.Arcade.Group {
    
    // constructor
    // arguments: the physics world, the game scene
    constructor(world: Phaser.Physics.Arcade.World, scene: Phaser.Scene) {
        super(world, scene);
    }
}

utils.ts

This file contains only one custom function, but I thought it was useful to group all custom functions in a separate file, to be reused whenever I need them.

Did not change.

// function to toss a random value between two elements in an array
// argument: an array with two items
export function randomValue(a: number[]): number {

    // return a random integer between the first and the second item of the array
    return Phaser.Math.Between(a[0], a[1]);
}

Then, about saw management, I added two new classes:

sawGroup.ts

Actually this class to extend the Phaser Group which contains all saws does not add any custom feature, but I preferred to create a custom class like I did with platformGroup.ts and enemyGroup.ts.

// SAW GROUP CLASS  

// saw group extends Arcade Group class
export default class SawGroup extends  Phaser.Physics.Arcade.Group {
    
    // constructor
    // arguments: the physics world, the game scene
    constructor(world: Phaser.Physics.Arcade.World, scene: Phaser.Scene) {
        super(world, scene);
    }
}

sawSprite.ts

This is the new saw class. Not that different from enemySprite.ts.

// SAW SPRITE CLASS    

// modules to import
import PlatformSprite from "./platformSprite";
import { randomValue } from './utils';
import { GameOptions } from './gameOptions';
import SawGroup from "./sawGroup";

// saw sprite extends Arcade Sprite class
export default class SawSprite extends Phaser.Physics.Arcade.Sprite {

    // the platform where the saw is patrolling
    platformToPatrol: PlatformSprite;

    // saw physics body
    body: Phaser.Physics.Arcade.Body;
    
    // constructor
    // arguments: the game scene, the platform where the saw is on, and saw group
    constructor(scene: Phaser.Scene, platform: PlatformSprite, group: SawGroup) {
        super(scene, platform.x, platform.y, 'saw');

        // add the saw to the scene
        scene.add.existing(this);

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

        // set saw scale
        this.scale = GameOptions.pixelScale;

        // play "saw" animation
        this.anims.play('saw', true);

        // add the saw to the group
        group.add(this);

        // determine body size, making it a bit smaller than sprite size
        let bodySize: number = this.displayWidth / 2 / this.scale * 0.8;

        // set saw body circular, and set some offset, manually unfortunately
        this.setCircle(bodySize, 4, 4);

        // saw enemy is patrolling the current platform
        this.platformToPatrol = platform;

        // set saw horizontal speed
        this.setVelocityX(randomValue(GameOptions.sawSpeedRange) * Phaser.Math.RND.sign());

        // set saw vertical speed
        this.setVelocityY(platform.body.velocity.y);

        // saw is not affected by gravity
        this.body.setAllowGravity(false);
    }

    // method to remove the saw from a group and place it into the pool
    // arguments: the group and the pool
    groupToPool(group: SawGroup, pool: SawSprite[]): void {

        // set saw velocity to zero
        this.setVelocity(0 ,0);

        // remove enemy from the group
        group.remove(this);

        // push the enemy in the pool
        pool.push(this);  
    }

    // method to remove the saw from the pool and place it into a group
    // arguments: the platform to patrol and the group
    poolToGroup(platform: PlatformSprite, group: SawGroup): void {

        // set the platform to patrol
        this.platformToPatrol = platform;

        // place the saw in the center of the platform
        this.x = platform.x;
        this.y = platform.y;

        // add the saw to the group
        group.add(this);

        // set saw horizontal speed
        this.setVelocityX(randomValue(GameOptions.sawSpeedRange) * Phaser.Math.RND.sign());

        // set saw vertical speed
        this.setVelocityY(platform.body.velocity.y);
    }

    // method to make the saw patrol a platform
    patrol(): void {

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

        // get saw bounds
        let sawBounds: Phaser.Geom.Rectangle = this.getBounds();

        // get saw horizontal speed
        let sawVelocityX: number = this.body.velocity.x
       
        // if the saw is moving left and is about to fall down the platform to the left side
        // or the saw is moving right and is about to fall down the platform to the right side
        if ((platformBounds.right + 25 < sawBounds.right && sawVelocityX > 0) || (platformBounds.left - 25 > sawBounds.left && sawVelocityX < 0)) {

            // invert saw horizontal speed
            this.setVelocityX(sawVelocityX * -1);
        }
    }    
}

And that’s it. Maybe we need another step to let player die properly rather than just restart the game. Download the source code of the entire project.

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