Adding Enemies
We are going to use a spider sprite to create “enemies” for an added complexity in the game mechanics.
Step 1 — Load the Spiders into the Game
In the preload() method of the Boot.ts file, add another spritesheet referencing a spider image.
// Boot.ts
import { Spider } from './Spider';
export class Boot extends Phaser.Scene {
//...
preload() {
//...
this.load.spritesheet('coin', 'images/coin_animated.png', {
frameWidth: 22,
frameHeight: 22,
});
this.load.spritesheet('spider', 'images/spider.png', {
frameWidth: 42,
frameHeight: 32,
});
this.load.audio('sfx:jump', 'audio/jump.wav');
//...
}
}
Step 2 — Setup the Spider.ts File
We are going to create a class that extends Phaser — creating a new game object representing a spider in the game.
- This allows the sprite to be displayed on the screen and interact with other objects in the physics of the game.
- export — makes this object available to other files. Every file we have worked in contains this.
- extends — makes the Spider inherit all of the properties and methods of Phaser. This class extends the Phaser class. Again, all of our files have been acting similarly.
- The constructor acts when a new instance of the Spider is created and takes three arguments:
- scene — which is an instance of the Play class (essentially, the scene of the game)
- x and y — coordinates where the spider gets created on the screen.
-
super(scene, x, y, 'spider'); — gives a reference to the “parent” constructor (in this case, Phaser) and passes the scene, x, and y arguments to the parent. This also includes a fourth argument: spider — which is the key for the image we set up in Boot.ts.
-
this.setOrigin(0.5, 0.5); — sets the origin of the spider sprite to its center. This is the basis for the object’s position, rotation, and scaling.
-
By default, the origin is always (0,0) — which is the top left.
-
We want all of these calculations to come from the center (which is (0.5, 0.5)).
// Spider.ts
import { Play } from './Play';
export class Spider extends Phaser.Physics.Arcade.Sprite {
constructor(scene: Play, x, y) {
super(scene, x, y, 'spider');
this.setOrigin(0.5, 0.5);
}
}
Step 3 — Make the Spiders Available in the Level.ts File
We need to declare a property within the Level class. We also want more than one spider enemy; so, we need to store an array of Spider objects (not just one).
- We will name this spiders which reflects that this is plural. This is what we will use to refer to the spiders throughout the file and add spiders to a level.
// Level.ts
//...
import { Spider } from './Spider';
export class Level {
hero!: Hero;
platforms!: Phaser.Physics.Arcade.StaticGroup;
groups!: { [key: string]: Phaser.Physics.Arcade.Group };
spiders: Spider[] = [];
//...
}
Add the Spiders to the Group in the Constructor
- Place the property spiders as an object of this.groups
- Within that, add a new physics group that is assigned to spiders.
// Level.ts
//...
import { Spider } from './Spider';
export class Level {
hero!: Hero;
platforms!: Phaser.Physics.Arcade.StaticGroup;
groups!: { [key: string]: Phaser.Physics.Arcade.Group };
spiders: Spider[] = [];
constructor(private scene: Play) {
this.platforms = this.scene.physics.add.staticGroup();
this.groups = {
players: this.scene.physics.add.group(),
coins: this.scene.physics.add.group({ allowGravity: false }),
spiders: this.scene.physics.add.group(),
};
}
//...
}
Create a method to spawn spiders in a level
- spawnSpiders(spiders) {...} - defines the spawnSpiders method, which takes one argument: spiders.
- The spiders argument is expected to be an array where each element is an object that contains the x and y coordinates where a spider should be spawned.
- spiders.forEach((spider) => {...}); - uses the forEach function to iterate over each spider in the spiders array. For each spider, it does the following:
- const _spider = new Spider(this.scene, spider.x, spider.y); - This line creates a new instance of the Spider class at the specified coordinates (this data comes from the level.json files and spawns the spiders according to the data for each level.
- this.groups.spiders.add(_spider, true); - This line adds the new spider sprite to the spiders group. The true argument means that the spider sprite will be immediately added to the scene.
- this.spiders = [...this.spiders, _spider]; - adds the new spider sprite to the spiders array. This array is used to keep track of all the spider sprites in the game.
- _spider.setCollideWorldBounds(true); - makes the spider sprite collide with the world bounds. This means that the spider sprite will not be able to move outside the game world on the screen.
// Level.ts
//...
import { Spider } from './Spider';
export class Level {
// ...
spawnSpiders(spiders) {
spiders.forEach((spider) => {
const _spider = new Spider(this.scene, spider.x, spider.y);
this.groups.spiders.add(_spider, true);
this.spiders = [...this.spiders, _spider];
_spider.setCollideWorldBounds(true);
});
}
//...
}
Incorporate this method in loadLevel(data):
// Level.ts
//...
import { Spider } from './Spider';
export class Level {
//...
loadLevel(data) {
//...
this.spawnSpiders(data.spiders);
}
spawnSpiders(spiders) {
spiders.forEach((spider) => {
const _spider = new Spider(this.scene, spider.x, spider.y);
this.groups.spiders.add(_spider, true);
this.spiders = [...this.spiders, _spider];
_spider.setCollideWorldBounds(true);
});
}
//...
}
Checkpoint
You should now see the spiders in the game. They should not be moving and should not be placed very well.
Step 4 — Add the Physics Collider for Spiders and Platforms
In the Play.ts file, we need to update the initPhysics() method so that spiders (as a group) are also bound by the platform objects similar to the hero.
- This creates a barrier between two groups of game objects so that the spiders can be “on” the platforms.
In a later step, we will add spiders!: Spider[]; as a property so that this action is being performed on the Spider class we have setup in Spider.ts.
// Play.ts
//...
import { Spider } from './Spider';
export class Play extends Phaser.Scene {
//...
initPhysics() {
this.physics.add.collider(this.hero, this.level.platforms);
this.physics.add.collider(this.groups.spiders, this.level.platforms);
//...
}
//...
}
Add the spiders to the mapProps()
- This gives a list of important properties to the game stored in an array so that we can dynamically access them throughout the game.
// Play.ts
//...
import { Spider } from './Spider';
export class Play extends Phaser.Scene {
//...
initPhysics() {
this.physics.add.collider(this.hero, this.level.platforms);
this.physics.add.collider(this.groups.spiders, this.level.platforms);
//...
}
//...
private mapProps() {
const props = [
'hero',
'groups',
'spiders',
];
props.forEach((prop) => (this[prop] = this.level[prop]));
}
//...
}
Step 5 — Give the Spiders the Ability to Move
In the Spider.ts file, create data to determine how the spider will move. This goes above the class (outside of its direct scope).
- const SPIDER_SPEED = 100; — sets the speed at which a spider character moves in pixels per second.
- const enum Directions {...} — an enum is a special type of TypeScript that allows for a collection of related values that can be numbers or strings.
- We assign the strings 'left' and 'right' to provide the possible directions a spider can move.
In the class, create a property named direction and assign it an initial value of Directions.right.
- This property will keep track of the current direction that the spider is moving in.
Finally, add an update() method.
// Spider.ts
import { Play } from './Play';
const SPIDER_SPEED = 100;
const enum Directions {
left = 'left',
right = 'right',
}
export class Spider extends Phaser.Physics.Arcade.Sprite {
direction = Directions.right;
constructor(scene: Play, x, y) {
super(scene, x, y, 'spider');
this.setOrigin(0.5, 0.5);
}
update() { }
}
Step 6 — In the Play.ts File, Add the spiders to the update Method
Similar to the hero — we need to connect the update in the Spider class to the gameplay. This method updates the state of the game objects during play.
- Add the spiders property to the Play class (with a required, non-null assertion). Make sure to correctly import Spider from the correct file.
- Add to the update() method including a forEach function to iterate over each spider in the array. We will need to include logic in the Spider class for how a spider is updated.
// Play.ts
export class Play extends Phaser.Scene {
hero!: Hero;
spiders!: Spider[];
level!: Level;
currentLevel: integer = 2;
//...
update() {
this.hero.update();
this.spiders.forEach((spider) => spider.update());
}
//...
}
Step 7 — Determine the Movement for the Spiders in the Spider.ts File
Create a crawl() method that allows the spider character to crawl in a specific direction.
- It should take one argument: (direction) — this is based on the enum and property we’ve already defined.
- It should call another method to set the speed of the crawl — getVelocity().
- It should set the horizontal velocity to the value received from this method using Phaser’s setVelocityX.
Create the contents of getVelocity() as a private method.
- This method will calculate the velocity of the spider based on the direction.
- It should take one argument: (direction) and return a value.
- If the direction is left — Directions.left — it should return: -SPIDER_SPEED.
- If the direction is right — Directions.right — it should return: SPIDER_SPEED.
- This value is defined at the top of the file.
- The positive and negative simply sets the right (positive) and left (negative) direction.
- We are using a ternary here for conditional logic (similar to an if/else statement).
Both of these should go below the constructor()
// Spider.ts
export class Spider extends Phaser.Physics.Arcade.Sprite {
direction = Directions.right;
//...
crawl(direction) {
const velocity = this.getVelocity(direction);
this.setVelocityX(velocity);
}
private getVelocity(direction) {
return direction === Directions.left ? SPIDER_SPEED : SPIDER_SPEED;
}
}
Step 8 — Setup Situations for the Live Context of Multiple Spiders
We need to control the behavior of the spiders during the game and constantly check its state to follow this behavior.
- The update() method will be used to constantly check the state and the live() method will be called to give the content of what it is checking for.
- live() establishes the behavior of the spiders to control their movement.
- This checks if the spider is touching or blocked on the right side — if it is, it changes the spider’s direction to the left.
- Then, if the spider is touching or blocked on the left side, it sets the spider’s direction to the right.
- Once the direction is correctly set, it calls the crawl() method to make the spider move in that direction and follow its speed that we’ve already set.
// Spider.ts
export class Spider extends Phaser.Physics.Arcade.Sprite {
direction = Directions.right;
constructor(scene: Play, x, y) {
super(scene, x, y, 'spider');
this.setOrigin(0.5, 0.5);
}
update() {
this.live();
}
live() {
if (this.body.touching.right || this.body.blocked.right) {
this.direction = Directions.left;
} else if (this.body.touching.left || this.body.blocked.left) {
this.direction = Directions.right;
}
this.crawl(this.direction);
}
crawl(direction) {
const velocity = this.getVelocity(direction);
this.setVelocityX(velocity);
}
private getVelocity(direction) {
return direction === Directions.left ? -SPIDER_SPEED : SPIDER_SPEED;
}
}
Step 9 — Create Barriers to Keep the Spiders on their Original Platforms
In the Boot.ts file, add an image file for an 'invisible-wall' from 'images/invisble_wall.png'.
// Boot.ts
import { Spider } from './Spider';
export class Boot extends Phaser.Scene {
//...
preload() {
this.load.image('grass:1x1', 'images/grass_1x1.png');
this.load.image('invisible-wall', 'images/invisible_wall.png');
//...
}
}
In the Level.ts file, give the class methods and properties to contain the spiders — we’ll call these “enemy walls” — which is a property within Phaser.
First, in the constructor, add a group called enemyWalls.
- allowGravity should be set to false and a second key — immovable — should be set to true. This makes sure that the wall is not affected by physics or actions within the game.
We will need a singular version to compose the basis of a wall and a plural version to set the collection of walls in a level.
-
spawnEnemyWall will take three parameters — an x coordinate, a y coordinate, and a side which will designate whether this goes on the left or right of a platform.
-
Define a variable called wall and use Phaser’s enemyWalls property to create the wall within the enemyWalls group. In the create() method, include the x and y coordinates and the string 'invisible-wall' which is a reference to the sprite for the wall in Phaser’s cache.
-
Then, set the origin of wall by using a ternary operator to determine if it will be left or right.
-
If side is 'left', the origin is set to (1, 1).
-
Otherwise, it will be set to (0, 1).
-
Finally, set the visible property of the wall to false. It can still function in the game world, but it won’t show up in the gameplay.
-
spawnEnemyWalls will take a platforms parameter — which gives access to the group of platform objects.
-
Use a forEach loop to iterate over each platform in the group and execute the code.
-
Using the x and y coordinates, spawn a wall on the left edge of the platform by declaring a variable left and assign the wall to the string 'left'.
-
Declare a variable right and calculate the “right” side of the platform so that the 'right' wall is set, as well.
Finally, add the walls in the loadLevel() method by calling the spawnEnemyWalls() method in it.
// Level.ts
export class Level {
//...
loadLevel(data) {
this.spawnBG();
this.spawnPlatforms(data.platforms);
this.spawnHero(data.hero);
this.spawnCoins(data.coins);
this.spawnSpiders(data.spiders);
this.spawnEnemyWalls(data.platforms);
}
spawnEnemyWalls(platforms) {
platforms.forEach((platform) => {
const left = this.spawnEnemyWall(platform.x, platform.y, 'left');
const right = this.spawnEnemyWall(
platform.x + platform.width,
platform.y,
'right'
);
});
}
spawnEnemyWall(x, y, side) {
const wall = this.groups.enemyWalls.create(x, y, 'invisible-wall');
wall.setOrigin(side === 'left' ? 1 : 0, 1);
wall.visible = false;
}
//...
}
Now, we can add another group to the constructor() {} for the enemy walls. This will take two unique properties: allowGravity: false and immovable: true so these walls remain static and are not affected by the physics of the game.
// Level.ts
export class Level {
//...
constructor(private scene: Play) {
this.platforms = this.scene.physics.add.staticGroup();
this.groups = {
//...
spiders: this.scene.physics.add.group(),
enemyWalls: this.scene.physics.add.group({
allowGravity: false,
immovable: true,
}),
};
}
//...
}
Your Level class should now look like this:
// Level.ts
import { Hero } from './Hero';
import { Play } from './Play';
import { Spider } from './Spider';
export class Level {
hero!: Hero;
platforms!: Phaser.Physics.Arcade.StaticGroup;
groups!: { [key: string]: Phaser.Physics.Arcade.Group };
spiders: Spider[] = [];
constructor(private scene: Play) {
this.platforms = this.scene.physics.add.staticGroup();
this.groups = {
players: this.scene.physics.add.group(),
coins: this.scene.physics.add.group({ allowGravity: false }),
spiders: this.scene.physics.add.group(),
enemyWalls: this.scene.physics.add.group({
allowGravity: false,
immovable: true,
}),
};
}
loadLevel(data) {
this.spawnBG();
this.spawnPlatforms(data.platforms);
this.spawnHero(data.hero);
this.spawnCoins(data.coins);
this.spawnSpiders(data.spiders);
this.spawnEnemyWalls(data.platforms);
}
spawnEnemyWalls(platforms) {
platforms.forEach((platform) => {
const left = this.spawnEnemyWall(platform.x, platform.y, 'left');
const right = this.spawnEnemyWall(
platform.x + platform.width,
platform.y,
'right'
);
});
}
spawnEnemyWall(x, y, side) {
const wall = this.groups.enemyWalls.create(x, y, 'invisible-wall');
wall.setOrigin(side === 'left' ? 1 : 0, 1);
wall.visible = false;
}
spawnSpiders(spiders) {
spiders.forEach((spider) => {
const _spider = new Spider(this.scene, spider.x, spider.y);
this.groups.spiders.add(_spider, true);
this.spiders = [...this.spiders, _spider];
_spider.setCollideWorldBounds(true);
});
}
spawnCoins(coins) {
coins.forEach((coin) => {
const _coin = this.spawnCoin(coin);
this.groups.coins.add(_coin, true);
});
}
spawnCoin(coin) {
const _coin = this.scene.add.sprite(coin.x, coin.y, 'coin');
_coin.setOrigin(0.5, 0.5);
return _coin;
}
spawnHero(hero) {
this.hero = new Hero(this.scene, hero.x, hero.y);
this.groups.players.add(this.hero, true);
this.hero.setBounce(0.3);
this.hero.setCollideWorldBounds(true);
}
spawnPlatforms(platforms) {
platforms.forEach((platform) => {
const _platform = this.spawnPlatform(platform);
this.platforms.add(_platform);
});
}
spawnPlatform(platform) {
const _platform = this.scene.add.sprite(
platform.x,
platform.y,
platform.image
);
_platform.setOrigin(0, 0);
return _platform;
}
spawnBG() {
const bg = this.scene.add.image(0, 0, 'background');
bg.setOrigin(0, 0);
}
}
Step 10 — Incorporate the “Enemy Walls” into the Physics of the Play Class
In the initPhysics() method, add another collider between the spiders group and the enemyWalls group.
// Play.ts
export class Play extends Phaser.Scene {
//...
initPhysics() {
this.physics.add.collider(this.hero, this.level.platforms);
this.physics.add.collider(this.groups.spiders, this.level.platforms);
this.physics.add.collider(this.groups.spiders, this.groups.enemyWalls);
this.physics.add.overlap(
this.hero,
this.groups.coins,
this.collectCoin,
undefined,
this
);
}
//...
}
Finally, in the Config.ts file, change the gameConfig by removing the comments (//) before the debug key so that the config now contains debug: true.
- This enables the the physics debug overlay so that we can see the enemy walls in place (as well as other physics elements).
import Phaser from "phaser";
import { Boot } from './Boot';
import { Play } from './Play';
export const gameConfig = {
type: Phaser.AUTO,
width: 960,
height: 600,
roundPixels: true,
backgroundColor: 0x000000,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: true,
},
},
scene: [Boot, Play],
};
Checkpoint
- The spiders should have their context set so they are on platforms.
- The spiders should move but also be contained to their platforms.
- The spiders should change direction if they come in contact with an object (especially the invisible enemy walls).
- The spiders should not affect or be affected by the hero.