Do you like my tutorials?

Then consider supporting me on Ko-fi

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.