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.