Talking about Serious Scramblers game, Game development, HTML5, Javascript, Phaser and TypeScript.
Time to add some serious enemies to Serious Scramblers prototype, so I am going to introduce patrolling enemies.
Patrolling enemies are enemies capable of patrolling platforms from left to right, no matter the length of the platform they are on, without falling down.
Also, patrolling enemies won’t be affected by platform types I added in previous step.
Moreover, these enemies are recycled by object pooling.
And of course, enemy are deadly, unless you stomp them by jumping or landing on their heads.
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.
There are three platform types:
White platform: normal platform.
Green platform: a bouncy platform, hero will bounce when landing on it. Bounce force can be configured in game options.
Red Platform: a disappearing platform. When the hero lands on it, the platform disappears after a certain amount of time which can also be configured in game options.
The game is made of 10 TypeScript files and one HTML file used in this prototype. I made a lot of changes since previous step, so I am not highlighting the new code, but each and every line has been commented.
index.html
The webpage which hosts the game, just the bare bones of HTML and main.ts
is called.
Also look at the thegame
div, this is where the game will run.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <! 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | // 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 } } } // game configuration object const configObject: Phaser.Types.Core.GameConfig = { type: Phaser.AUTO, backgroundColor:0x444444, 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 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( 'hero' , 'assets/hero.png' ); this .load.image( 'platform' , 'assets/platform.png' ); this .load.image( 'enemy' , 'assets/enemy.png' ); } // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // CONFIGURABLE GAME OPTIONS export const GameOptions = { // game size, in pixels gameSize: { width: 750, height: 1334 }, // 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: 500, // hero speed, in pixels per second heroSpeed: 300, // platform speed, in pixels per second platformSpeed: 120, // 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], // chances of an enemy appearing on a platform, 0: no chance, 1: certainly appears enemyChance: 1 } |
playGame.ts
The game itself, the biggest class, game logic is stored here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 | // THE GAME ITSELF // modules to import import { GameOptions } from './gameOptions' ; import PlayerSprite from './playerSprite' ; import PlatformSprite from './platformSprite' ; import EnemySprite from './enemySprite' ; import PlatformGroup from './platformGroup' ; import EnemyGroup from './enemyGroup' ; // 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; // 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[]; // just a debug text to print some info debugText: Phaser.GameObjects.Text; // constructor constructor() { super ({ key: 'PlayGame' }); } // method to be called once the class has been created create(): void { // add the debug text to the game this .debugText = this .add.text(16, 16, '' , { color: '#7fdbff' , fontFamily: 'monospace' , fontSize: '32px' }); // initialize enemy pool as an empty array this .enemyPool = []; // 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 ); // 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); // 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 place stuff on platform // argument: the platform placeStuffOnPlatform(platform: PlatformSprite): void { // should we add an enemy? if (Math.random() < GameOptions.enemyChance) { // 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); } } } // method to move the hero // argument: the input pointer moveHero(e: Phaser.Input.Pointer): void { // set hero velocity according to input horizontal coordinate this .hero.setVelocityX(GameOptions.heroSpeed * ((e.x > GameOptions.gameSize.width / 2) ? 1 : -1)); // 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); } } // method to stop the hero stopHero(): void { // ... just stop the hero :) this .hero.setVelocityX(0); } // 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); // 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 { // update debug text this.debugText.setText(' Enemy Group: ' + this .enemyGroup.countActive( true ) + "\nEnemy Pool: " + this .enemyPool.length); // 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 ); // 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 enemies let enemies: EnemySprite[] = this .enemyGroup.getChildren() as EnemySprite[]; // 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 left the screen... if (enemyBounds.bottom < 0 || (enemyBounds.top > GameOptions.gameSize.height && enemy.body.velocity.y > 500)) { // move enemy from enemy group to enemy pool enemy.groupToPool( this .enemyGroup, this .enemyPool); } } // 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // 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 { // 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 ); } } |
platformSprite.ts
Class to define the platforms.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | // PLATFORM SPRITE CLASS // modules to import import PlatformGroup from './platformGroup' ; import { GameOptions } from './gameOptions' ; import { randomValue } from './utils' ; // platform sprite extends Arcade Sprite class export default class PlatformSprite extends Phaser.Physics.Arcade.Sprite { // 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; // constructor // arguments: the game scene, and the platform group constructor(scene: Phaser.Scene, group: PlatformGroup) { super (scene, 0, 0, 'platform' ); // 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(); } // 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 = Phaser.Math.Between(0, 2); } // platform width this .displayWidth = randomValue(GameOptions.platformLengthRange); // set platform tint according to platform type this .setTint(GameOptions.platformColors[ this .platformType]); } } |
platformGroup.ts
Class to define the Phaser Group, dedicated to the group which contains all platforms.
The method to retrieve the lowest platform position has been moved here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // ENEMY 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | // 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 ); // 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()); } // method to make the enemy patrol a platform patrol(): void { // get platform bounds let platformBounds = this .platformToPatrol.getBounds(); // get enemy bounds let enemyBounds = this .getBounds(); // get enemy horizontal speeds let enemyVelocityX = 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 < enemyBounds.right && enemyVelocityX > 0) || (platformBounds.left > 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 ); // set the enemy invisible this .setVisible( false ); } // 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 - 100; // set the enemy visible this .setVisible( true ); // add the enemy to the group group.add( this ); // set enemy horizontal speed this .setVelocityX(randomValue(GameOptions.enemyPatrolSpeedRange) * Phaser.Math.RND.sign()); } } |
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
.
1 2 3 4 5 6 7 8 9 10 11 | // 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.
And now we have patrolling enemies. Download the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.