Adding a boss… Charizard

  • Post author:
  • Post category:Beginner

In this post we’ll be adding a final boss to our game. So far, we’ve created a platformer game where we can collect stars and avoid bombs. We’ve added multiplayer, with Pikachu as our friendly team mate. Now, let’s complete this Platformer course by adding a final boss, Charizard!!! He follows you around and will kick and stun you if he gets close enough. You can try out the game here and take a look at the final code we’ll write, here.

As we know, Charizard is the evolved form of Charmeleon and the final evolution of Charmander. He is one of the coolest Pokemon out there and is the perfect candidate for the final boss for our game.

Setup

Let’s start off with the Glitch project that added multiplayer (don’t forget to click Remix to edit), or, feel free to go through the multiplayer lesson as a refresher.

Adding Charizard

Next, we’ll add a Charizard sprite sheet to our game. I found a great sprite sheet on spriters-resource.com (link), made by JoshR691:

Later, we’ll learn how to crop and create our own sprite sheet from those we find on the web. For now, you can use the included the sprite sheet in our Glitch project, or download the cropped sprite sheet here and add it to your own Glitch project.

In the assets folder I’ve added the charizard sprite sheet

Next, we’ll declare a boss variable, load it up as a sprite sheet, and add it to our game:

import Phaser from '/phaser.js';
var player;
var player2;
var cursors;
var stars;
var score = 0;
var scoreText;
var bombs;
var keys;
// Add this new variable below
var boss;

In the preload function, we’ll load the sprite sheet, like before. We set the frameWidth to 64 and the frameHeight to 52, to match the Charizard walking frames in our sprite sheet:

function preload() {
// I've skipped showing our previous code
// Add the following code until the "stop" comment
this.load.spritesheet(
'charizard',
'https://cdn.glitch.com/0e40d76a-3297-4129-808d-27fb1a51ee4f%2Fcharizard-walking.png?v=1630642084621',
{ frameWidth: 64, frameHeight: 52 }
);
// stop
}

Next we’ll add an animation to our create function to show Charizard walking. Add the following code the bottom of the function:

function create() {
// I've skipped showing our previous code
// Add the following code until the "stop" comment
this.anims.create({
key: 'charizard-right',
frames: this.anims.generateFrameNumbers('charizard', {
start: 0,
end: 2,
}),
frameRate: 10,
repeat: -1,
});
// stop
}

In lines 5-13 above, we create a new animation, charizard-right which is made up of the 3 frames of Charizard walking in our sprite sheet.

Next, we’ll add the boss to our game, but disable him, so he doesn’t immediately show up. Copy the highlighted code to the bottom of our create() function:

function create() {
// I've skipped the code we wrote before
// Copy the code below until the "stop" comment
boss = this.physics.add.sprite(250, 0, "charizard");
boss.setScale(2);
boss.setBounce(0.1);
boss.setCollideWorldBounds(true);
this.physics.add.collider(boss, platforms);
boss.disableBody(true, true);
// stop
}

In lines 5-9, we set the properties as we did for our other sprites. In line 10, we disable the sprite so the boss doesn’t show up right away.

Now let’s make Charizard show up after we collect 12 stars, by adding the highlighted code in our collectStar(player, star) function:

function collectStar(player, star) {
star.disableBody(true, true);
score += 10;
scoreText.setText("Score: " + score);
if (score >= 120) {
boss.enableBody(true, boss.x, boss.y, true, true);
}
if (stars.countActive(true) === 0) {
stars.children.iterate(function(child) {
child.enableBody(true, child.x, 0, true, true);
});
var x =
player.x < 400
? Phaser.Math.Between(400, 800)
: Phaser.Math.Between(0, 400);
var bomb = bombs.create(x, 16, "bomb");
bomb.setBounce(1);
bomb.setCollideWorldBounds(true);
bomb.setVelocity(Phaser.Math.Between(-200, 200), 20);
bomb.allowGravity = false;
}
}

In lines 7-9, we check how many stars have been collected. Since each star is worth 10, then after we’ve collected 12 stars the score would be 120. That’s when we “enable” our boss and Charizard will show up!

Charizard shows up after collecting 120 points. Don’t forget about the bombs!

Making Charizard smart (adding Artificial Intelligence)

At this point, Charizard is pretty dumb. After collecting 12 stars, he appears and just stands there while we collect stars and avoid bombs. Now we’re going to add some Artificial Intelligence. In particular, we’ll make Charizard chase the closest player.

Charizard’s brain will be quite simple:

  1. Find the closest player (that’s alive)
  2. Move toward that player

Let’s start by writing out the high-level logic, highlighted below:

function findTarget() {
// always return the first player, for now
return player;
}
function updateBoss() {
const target = findTarget();
const deltaX = target.x - boss.x;
const sign = Math.sign(deltaX);
if (Math.abs(deltaX) > 50) {
boss.setVelocityX(sign * 50);
boss.flipX = sign < 0;
boss.anims.play("charizard-right", true);
}
}
function update() {
if (boss.active) {
updateBoss();
}
if (player.isAlive) {
updatePlayerOne();
}
if (player2.isAlive) {
updatePlayerTwo();
}
}

Starting on the bottom, line 19 checks if the boss is active. Remember, our boss, Charizard, is only active after our players have collected 12 stars. If the our boss is active, then we should go ahead and call the updateBoss() function, on line 20.

Lines 7-15 define our basic Charizard brain (or Artificial Intelligence). Line 7 will find a target by calling the findTarget() function. This function currently always returns the first player. We’ll make this smarter later.

Lines 8-15 make Charizard move toward the target, and is worth some explanation.

Line 8 defines variable deltaX, which is the difference between target.x and boss.x:

const deltaX = target.x - boss.x;

Visually we’re computing the distance along the x-axis between the target’s x-position, and the boss’s x-position. Remember that a game object’s x value is always measured from the left-side of the screen:

const deltaX = player.x - boss.x;

With deltaX, line 9 calculates the sign, which will either be 1 or -1, depending of if deltaX is positive or negative:

  const sign = Math.sign(deltaX);

When deltaX is positive, it means the target is to the right of the boss. On the other hand, if deltaX is negative, the target will be to the left of the boss. So, we can use the sign to orient Charizard and move him in the appropriate direction.

Line 11 checks how far away Charizard is from his target:

if (Math.abs(deltaX) > 50) 

We use Math.abs() here to calculate the absolute value of a number. This just removes the negative sign if it exists (i.e. -1 turns to 1). That way we can compare if Charizard is 50 pixels away, regardless if he’s to the left or right of his target. If Charizard is far enough away, then line 12 moves Charizard closer to the target (notice that sign will make the velocity positive or negative):

boss.setVelocityX(sign * 50);

Line 13 will flip Charizard, depending on the sign:

boss.flipX = sign < 0;

The sign < 0 expression returns a boolean (true or false), depending if sign less than 0 or not. This boolean would be used to flip Charizard.

Finally, line 14 plays the Charizard walking animation we loaded earlier.

Let’s try it out!

Charizard chases player 1

Now let’s make Charizard find the closest player (that’s alive), by updating the findTarget function:

function findTarget() {
let target;
if (player.isAlive && player2.isAlive) {
const bossToPlayerDist = Phaser.Math.Distance.BetweenPoints(boss, player);
const bossToPlayer2Dist = Phaser.Math.Distance.BetweenPoints(boss, player2);
target = bossToPlayerDist < bossToPlayer2Dist ? player : player2;
} else {
target = player.isAlive ? player : player2;
}
return target;
}

In line 2 we declare a target variable, which is what we’ll return at the end of the function (line 13).

In line 4, we’ll check if both players are alive. If so, pick the closest player. If only one player is alive, then Charizard will always target that remaining player (line 10).

Ok, suppose both players are alive. We’ll need to find the “closest” player to Charizard by calculating 2 distances:

  1. The distance from the boss to player 1 (that’s the bossToPlayerDist variable)
  2. The distance from the boss to player 2 (that’s the bossToPlayer2Dist variable)

Charizard’s final target is the closest player (line 8). Note, we could have used the Phaser.Math.Distance.BetweenPointsSquared function for faster calculation. Later, we’ll dive into how distances are calculated (via the Pythagorean theorem) to show why the squared function is faster.

Now if we run our game, we’ll see Charizard chasing the closest player, if both are alive. If only one player is alive, then Charizard will go after that player. However, when Charizard gets to his target, nothing happens. This is because we haven’t coded the collision behavior. Let’s do that next.

Adding Charizard’s collision behavior

Like we did for player collision, at the bottom of the create() function we’ll add the following highlighted code. We’ll also create a new callback function bossCollide, which is called whenever the boss collides with either player:

function create() {
// Skipped showing the code we wrote earlier
// Add the following code until the stop comment
this.physics.add.collider(boss, player, bossCollide, null, this);
this.physics.add.collider(boss, player2, bossCollide, null, this);
// stop
}
function bossCollide(boss, pl) {
pl.isAlive = false;
pl.setTint(0xff0000);
pl.anims.pause();
pl.setVelocityX(0);
const kickVelocity = (pl.x - boss.x) * 4;
pl.setVelocityX(kickVelocity);
pl.setVelocityY(-400);
}

Lines 5-6 tell our physics engine to call the bossCollide callback function whenever the boss collides with player or player2. Lines 11-14 “kill” our player, in the same way that a bomb does. In fact, this is a copied code from the hitBomb callback function. Later, we’ll refactor this code so the same logic isn’t copied in two places.

Line 16-17 calculate a kickVelocity, which is a horizontal value based on where the player is, relative to Charizard, and set it on the player. This effectively pushes the player backward. Line 18 adds a vertical push to the player, effectively bouncing them upward as well. Let’s try this out!

Charizard kicks hard!

One more thing we need to do is move our logic to check if the game is over. Before, we checked both players were alive after any player collided with a bomb, in the hitBomb function. However, now Charizard can also defeat a player, so we should move this check into the update function. So the two functions should now look like this:

function update() {
if (!player.isAlive && !player2.isAlive) {
this.physics.pause();
this.add.text(200, 100, "game over!", {
fontSize: "64px",
fill: "#000"
});
}
if (boss.active) {
updateBoss();
}
if (player.isAlive) {
updatePlayerOne();
}
if (player2.isAlive) {
updatePlayerTwo();
}
}
function hitBomb(pl, bomb) {
pl.isAlive = false;
pl.setTint(0xff0000);
pl.anims.pause();
pl.setVelocityX(0);
}

Defeating Charizard

Right now, Charizard is literally invincible; nothing can defeat him. Well, the reason is because we haven’t coded any logic for it! There many ways we can create a weakness for Charizard, but to keep things simple, let’s add a vunerability on top of Charizard’s head. So, if any player is able to stomp on his head, he should get hurt.

How do we represent Charizard’s health? We could use hearts, or some text, or other icons. By now, we should be able to add that if we wanted to track it that way. Instead, let’s use Charizard’s size to indicate his health. In other words, whenever Charizard is injured, he’ll shrink. When he gets to small enough size, we’ll declare victory, and show the “You win” screen.

Let’s add this highlighted code in the bossCollide function:

function bossCollide(boss, pl) {
if (boss.body.touching.up) {
boss.scale /= 2.0;
pl.setVelocityY(-100);
if (boss.scale < 0.5) {
boss.scene.physics.pause();
boss.scene.add.text(200, 100, "You win!", {
fontSize: "64px",
fill: "#000"
});
}
return;
}
pl.isAlive = false;
pl.setTint(0xff0000);
pl.anims.pause();
pl.setVelocityX(0);
const kickVelocity = (pl.x - boss.x) * 4;
pl.setVelocityX(kickVelocity);
pl.setVelocityY(-400);
}

In line 2, we check if Charizard is being collided from the top (like, somebody is stomping on his head). If so, then we’ll scale him down by half (line 3). This syntax may look unfamiliar to new coders. It’s basically shorthand (or “syntactic sugar”) for:

boss.scale = boss.scale / 2.0;

In line 4 we bounce the player up a bit, so it’s like the player jumped off of Charizard’s head.

In line 5, we check Charizard’s scale: if he’s too small, then he’s been defeated. Then we can pause the game (line 6) and show the “you win” screen (lines 7-10). Let’s try it out!

Congratulations! We’ve completed our game!!

Bonus: refactoring code for clarity

Often times during coding, we’ll find that code we’ve written before could be updated to be more clear, even though there’s no change in functionality.

Rearranging code for clarity, without changing behavior is called refactoring.

When we added logic for Charizard to defeat our player, we noticed it was the same code as when a bomb collides with a player. Copying code is generally not good since it’s redundant. Even more dangerous, the logic isn’t shared, so if we decide to change how a player is defeated, we’d have to change it in two places! Instead let’s refactor the player-defeat code into a function defeatPlayer and call it from the bossCollide and hitBomb functions:

function defeatPlayer(pl) {
pl.isAlive = false;
pl.setTint(0xff0000);
pl.anims.pause();
pl.setVelocityX(0);
}
function hitBomb(pl, bomb) {
defeatPlayer(pl);
}
function bossCollide(boss, pl) {
if (boss.body.touching.up) {
boss.scale /= 2.0;
pl.setVelocityY(-100);
if (boss.scale < 0.5) {
boss.scene.physics.pause();
boss.scene.add.text(200, 100, "You win!", {
fontSize: "64px",
fill: "#000"
});
}
return;
}
defeatPlayer(pl);
const kickVelocity = (pl.x - boss.x) * 4;
pl.setVelocityX(kickVelocity);
pl.setVelocityY(-400);
}

Conclusion

Congratulations on completing this lesson on adding Charizard the boss! We learned:

  1. How to add Artificial Intelligence to our game
  2. How to simulate a “kick” to a player
  3. How to refactor code for clarity

This also completes the Platformer course. If you enjoyed this course and want to learn more, check out tinkercode.net for more courses on 3D games, VR games, and more!