Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bb6e329
Update lesson 1 "Initialize the framework" to Phaser 3.90 (except for…
igrep Jul 13, 2025
ccb6b7f
Forgot to update the URL to the example
igrep Jul 13, 2025
d3ae4c6
Update lesson 2 "Scaling" to Phaser 3.90 (except for the live sample)
igrep Jul 13, 2025
4064490
Update lesson 3 "Load the assets and print them on screen" to Phaser …
igrep Jul 15, 2025
d3e834a
Update lesson 4 "Move the ball" to Phaser 3.90 (except for the live s…
igrep Jul 16, 2025
6c4d861
Update lesson 5 "Physics" to Phaser 3.90 (except for the live sample)
igrep Jul 17, 2025
cda4007
Update lesson 6 "Bounce off the walls" to Phaser 3.90 (except for the…
igrep Jul 19, 2025
3a1c01e
(WIP) Update lesson 7 "Player paddle and controls" to Phaser 3.90
igrep Jul 20, 2025
3deac1b
Update lesson 7 "Player paddle and controls" to Phaser 3.90 except fo…
igrep Jul 21, 2025
19a5a93
Update the live sample in lesson 7 "Player paddle and controls" to Ph…
igrep Jul 21, 2025
5eadc1e
Correct the path to the assets in the live sample
igrep Jul 23, 2025
753cf50
Update lesson 8 "Game over" to Phaser 3.90
igrep Jul 23, 2025
0ce3ffd
Update lesson 9 "Build the brick field" to Phaser 3.90
igrep Jul 26, 2025
083a83a
Update lesson 10 "Collision detection" to Phaser 3.90
igrep Jul 26, 2025
a557f4b
Update lesson 11 "The score" to Phaser 3.90
igrep Jul 26, 2025
3014535
Modify lesson 11 "The score" to Phaser 3.90
igrep Jul 26, 2025
32d2919
Modify lesson 11 "The score" to Phaser 3.90
igrep Jul 26, 2025
6429f21
Modify lesson 12 "Win the game" to Phaser 3.90
igrep Jul 26, 2025
14b3dcd
(WIP) Modify lesson 13 "Extra lives" to Phaser 3.90
igrep Jul 27, 2025
5b37cab
Modify lesson 13 "Extra lives" to Phaser 3.90
igrep Jul 28, 2025
e40e328
Modify lesson 14 "Extra lives" to Phaser 3.90
igrep Jul 28, 2025
b3e64ab
(WIP) Modify lesson 15 "Buttons" to Phaser 3.90
igrep Jul 29, 2025
59be5d9
Modify lesson 15 "Buttons" to Phaser 3.90
igrep Jul 31, 2025
26ec9c4
Modify lesson 16 "Randomizing gameplay" to Phaser 3.90
igrep Jul 31, 2025
4ef696f
Migrate all the lessons to live sample from JSFiddle
igrep Jul 31, 2025
a5e4b71
Fix syntax errors in the example code and several parts I forgot to fix
igrep Aug 1, 2025
854560b
Apply formatter
igrep Aug 2, 2025
a186193
Updates
Josh-Cena Aug 2, 2025
16fdcc0
Fix grammar
Josh-Cena Aug 2, 2025
3287b45
Final tweaks
Josh-Cena Aug 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,98 +7,320 @@ sidebar: games

{{PreviousNext("Games/Workflows/2D_Breakout_game_Phaser/Extra_lives", "Games/Workflows/2D_Breakout_game_Phaser/Buttons")}}

This is the **14th step** out of 16 of the [Gamedev Phaser tutorial](/en-US/docs/Games/Tutorials/2D_breakout_game_Phaser). You can find the source code as it should look after completing this lesson at [Gamedev-Phaser-Content-Kit/demos/lesson14.html](https://github.com/end3r/Gamedev-Phaser-Content-Kit/blob/gh-pages/demos/lesson14.html).
This is the **14th step** out of 16 of the [Gamedev Phaser tutorial](/en-US/docs/Games/Tutorials/2D_breakout_game_Phaser). You can find the source code as it should look after completing this lesson at [2D_Breakout_game_Phaser/lesson14.html](https://github.com/igrep/2D_Breakout_game_Phaser/blob/main/lesson14.html).

To make the game look more juicy and alive we can use animations and tweens. This will result in a better, more entertaining experience. Let's explore how to implement Phaser animations and tweens in our game.

## Animations

In Phaser, animations, involve taking a spritesheet from an external source and displaying the sprites sequentially. As an example, we will make the ball wobble when it hits something.

First of all, [grab the spritesheet from GitHub](https://github.com/end3r/Gamedev-Phaser-Content-Kit/blob/gh-pages/demos/img/wobble.png) and save it in your `/img` directory.
First of all, [grab the spritesheet from GitHub](https://github.com/igrep/2D_Breakout_game_Phaser/blob/main/img/wobble.png) and save it in your `/img` directory.

Next, we will load the spritesheet — put the following line at the bottom of your `preload()` function:
Next, we will load the spritesheet — put the following line at the bottom of your `preload` method:

```js
game.load.spritesheet("ball", "img/wobble.png", 20, 20);
this.load.spritesheet("wobble", "img/wobble.png", {
frameWidth: 20,
frameHeight: 20,
});
```

Instead of loading a single image of the ball we can load the whole spritesheet — a collection of different images. We will show the sprites sequentially to create the illusion of animation. The `spritesheet()` method's two extra parameters determine the width and height of each single frame in the given spritesheet file, indicating to the program how to chop it up to get the individual frames.
Instead of loading a single image of the ball we can load the whole spritesheet — a collection of different images. We will show the sprites sequentially to create the illusion of animation. The `spritesheet` method's extra parameter determine the width and height of each single frame in the given spritesheet file, indicating to the program how to chop it up to get the individual frames.

## Loading the animation

Next up, go into your create() function, find the line that loads the ball sprite, and below it put the call to `animations.add()` seen below:
Next up, go into your `create` method, find the code block that loads and configures the ball sprite, and below it, put the call to `anims.create` seen below:

```js
ball = game.add.sprite(50, 250, "ball");
ball.animations.add("wobble", [0, 1, 0, 2, 0, 1, 0, 2, 0], 24);
this.ball = this.add.sprite(
this.scale.width * 0.5,
this.scale.height - 25,
"ball",
);
// ...
this.ball.anims.create({
key: "wobble",
frameRate: 24,
frames: this.anims.generateFrameNumbers("wobble", {
frames: [0, 1, 0, 2, 0, 1, 0, 2, 0],
}),
});
```

To add an animation to the object we use the `animations.add()` method, which contains the following parameters
To add an animation to the object we use the `anims.create` method, which receives the parameter with the following properties:

- The name we chose for the animation
- An array defining the order in which to display the frames during the animation. If you look again at the `wobble.png` image, you'll see there are three frames. Phaser extracts these and stores references to them in an array — positions 0, 1, and 2. The above array says that we are displaying frame 0, then 1, then 0, etc.
- The frame rate, in fps. Since we are running the animation at 24fps and there are 9 frames, the animation will display just under three times per second.
- `key`: The name we chose for the animation
- `frameRate`: The frame rate, in fps. Since we are running the animation at 24fps and there are 9 frames, the animation will display just under three times per second.
- `frames`: An array defining the order in which to display the frames during the animation. If you look again at the `wobble.png` image, you'll see there are three frames. Phaser extracts these and stores references to them in an array — positions 0, 1, and 2. The above array says that we are displaying frame 0, then 1, then 0, etc.

## Applying the animation when the ball hits the paddle

In the `arcade.collide()` method call that handles the collision between the ball and the paddle (the first line inside `update()`, see below) we can add an extra parameter that specifies a function to be executed every time the collision happens, in the same fashion as the `ballHitBrick()` function. Update the first line inside `update()` as shown below:
In the `physics.collide` method call that handles the collision between the ball and the paddle (the first line inside `update`, see below), we can add an extra parameter that specifies a function to be executed every time the collision happens, in the same fashion as the `hitBrick` method. Update the first line inside `update()` as shown below:

```js
function update() {
game.physics.arcade.collide(ball, paddle, ballHitPaddle);
game.physics.arcade.collide(ball, bricks, ballHitBrick);
paddle.x = game.input.x || game.world.width * 0.5;
class Example extends Phaser.Scene {
// ...
update() {
this.physics.collide(this.ball, this.paddle, this.hitPaddle.bind(this));
this.physics.collide(this.ball, this.bricks, this.hitBrick.bind(this));
this.paddle.x = this.input.x || this.scale.width * 0.5;
}
// ...
}
```

Then we can create the `ballHitPaddle()` function (having `ball` and `paddle` as default parameters), playing the wobble animation when it is called. Add the following function just before your closing `</script>` tag:
Then, we can create the `hitPaddle` method (having `ball` and `paddle` as default parameters), playing the wobble animation when it is called. Add the following method just before your closing brace `}` of the `Example` class:

```js
function ballHitPaddle(ball, paddle) {
ball.animations.play("wobble");
class Example extends Phaser.Scene {
// ...
hitPaddle(ball, paddle) {
this.ball.anims.play("wobble");
}
// ...
}
```

The animation is played every time the ball hits the paddle. You can add the `animations.play()` call inside the `ballHitBrick()` function too, if you feel it would make the game look better.
The animation is played every time the ball hits the paddle. You can add the `anims.play` call inside the `hitBrick()` method too, if you feel it would make the game look better.

## Tweens

Whereas animations play external sprites sequentially, tweens smoothly animate properties of an object in the gameworld, such as width or opacity.

Let's add a tween to our game to make the bricks smoothly disappear when they are hit by the ball. Go to your `ballHitBrick()` function, find your `brick.kill();` line, and replace it with the following:
Let's add a tween to our game to make the bricks smoothly disappear when they are hit by the ball. Go to your `hitBrick` method, find your `brick.destroy();` line, and replace it with the following:

```js
const killTween = game.add.tween(brick.scale);
killTween.to({ x: 0, y: 0 }, 200, Phaser.Easing.Linear.None);
killTween.onComplete.addOnce(() => {
brick.kill();
}, this);
killTween.start();
const destroyTween = this.tweens.add({
targets: brick,
ease: "Linear",
repeat: 0,
duration: 200,
props: {
scaleX: 0,
scaleY: 0,
},
onComplete: () => {
brick.destroy();
},
});
destroyTween.play();
```

Let's walk through this so you can see what's happening here:

1. When defining a new tween you have to specify which property will be tweened — in our case, instead of hiding the bricks instantly when hit by the ball, we will make their width and height scale to zero, so they will nicely disappear. To the end, we use the `add.tween()` method, specifying `brick.scale` as the argument as this is what we want to tween.
2. The `to()` method defines the state of the object at the end of the tween. It takes an object containing the chosen parameter's desired ending values (scale takes a scale value, 1 being 100% of size, 0 being 0% of size, etc.), the time of the tween in milliseconds and the type of easing to use for the tween.
1. When defining a new tween you have to specify which property of the `targets` will be tweened — in our case, instead of hiding the bricks instantly when hit by the ball, we will make their width and height scale to zero, so they will nicely disappear. To the end, we use the `tweens.add` method, specifying `brick` as the `targets` and the `scaleX` and `scaleY` properties to tween in the `props` object.
2. Other properties we can set are `ease`, which defines the easing function to use (in this case, `Linear`), `repeat`, which defines how many times the tween should repeat (0 means it will not repeat), and `duration`, which is the time in milliseconds that the tween will take to complete.
3. We will also add the optional `onComplete` event handler, which defines a function to be executed when the tween finishes.
4. The last thing do to is to start the tween right away using `start()`.
4. The last thing do to is to start the tween right away using the `play` method.

That's the expanded version of the tween definition, but we can also use the shorthand syntax:
## Compare your code

```js
game.add
.tween(brick.scale)
.to({ x: 2, y: 2 }, 500, Phaser.Easing.Elastic.Out, true, 100);
You can check the finished code for this lesson in the live demo below, and play with it to understand better how it works:

```html hidden live-sample__final
<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.90.0/phaser.js"></script>
```

This tween will double the brick's scale in half a second using Elastic easing, will start automatically, and have a delay of 100 milliseconds.
```css hidden live-sample__final
* {
padding: 0;
margin: 0;
}
```

## Compare your code
```js hidden live-sample__final
class Example extends Phaser.Scene {
ball;
paddle;
bricks;

scoreText;
score = 0;

lives = 3;
livesText;
lifeLostText;

preload() {
this.load.setBaseURL(
"https://mdn.github.io/shared-assets/images/examples/2D_breakout_game_Phaser",
);

this.load.image("ball", "ball.png");
this.load.image("paddle", "paddle.png");
this.load.image("brick", "brick.png");
this.load.spritesheet("wobble", "wobble.png", {
frameWidth: 20,
frameHeight: 20,
});
}
create() {
this.ball = this.add.sprite(
this.scale.width * 0.5,
this.scale.height - 25,
"ball",
);
this.physics.add.existing(this.ball);
this.ball.body.setVelocity(150, -150);
this.ball.body.setCollideWorldBounds(true, 1, 1);
this.ball.body.setBounce(1);
this.ball.anims.create({
key: "wobble",
frameRate: 24,
frames: this.anims.generateFrameNumbers("wobble", {
frames: [0, 1, 0, 2, 0, 1, 0, 2, 0],
}),
});

this.paddle = this.add.sprite(
this.scale.width * 0.5,
this.scale.height - 5,
"paddle",
);
this.paddle.setOrigin(0.5, 1);
this.physics.add.existing(this.paddle);
this.paddle.body.setImmovable(true);

this.physics.world.checkCollision.down = false;
this.ball.body.onWorldBounds = true;

this.initBricks();

const textStyle = { font: "18px Arial", fill: "#0095DD" };
this.scoreText = this.add.text(5, 5, "Points: 0", textStyle);

this.livesText = this.add.text(
this.scale.width - 5,
5,
`Lives: ${this.lives}`,
textStyle,
);
this.livesText.setOrigin(1, 0);
this.lifeLostText = this.add.text(
this.scale.width * 0.5,
this.scale.height * 0.5,
"Life lost, click to continue",
textStyle,
);
this.lifeLostText.setOrigin(0.5, 0.5);
this.lifeLostText.visible = false;
}
update() {
this.physics.collide(this.ball, this.paddle, this.hitPaddle.bind(this));
this.physics.collide(this.ball, this.bricks, this.hitBrick.bind(this));

this.paddle.x = this.input.x || this.scale.width * 0.5;
const ballIsOutOfBounds = !Phaser.Geom.Rectangle.Overlaps(
this.physics.world.bounds,
this.ball.getBounds(),
);
if (ballIsOutOfBounds) {
this.ballLeaveScreen();
}
}

initBricks() {
const bricksLayout = {
width: 50,
height: 20,
count: {
row: 3,
col: 7,
},
offset: {
top: 50,
left: 60,
},
padding: 10,
};

this.bricks = this.add.group();
for (let c = 0; c < bricksLayout.count.col; c++) {
for (let r = 0; r < bricksLayout.count.row; r++) {
const brickX =
c * (bricksLayout.width + bricksLayout.padding) +
bricksLayout.offset.left;
const brickY =
r * (bricksLayout.height + bricksLayout.padding) +
bricksLayout.offset.top;

const newBrick = this.add.sprite(brickX, brickY, "brick");
this.physics.add.existing(newBrick);
newBrick.body.setImmovable(true);
this.bricks.add(newBrick);
}
}
}

hitPaddle(ball, paddle) {
this.ball.anims.play("wobble");
}

hitBrick(ball, brick) {
const destroyTween = this.tweens.add({
targets: brick,
ease: "Linear",
repeat: 0,
duration: 200,
props: {
scaleX: 0,
scaleY: 0,
},
onComplete: () => {
brick.destroy();
},
});
destroyTween.play();
this.score += 10;
this.scoreText.setText(`Points: ${this.score}`);

if (this.bricks.countActive() === 0) {
alert("You won the game, congratulations!");
location.reload();
}
}

ballLeaveScreen() {
this.lives--;
if (this.lives > 0) {
this.livesText.setText(`Lives: ${this.lives}`);
this.lifeLostText.visible = true;
this.ball.body.reset(this.scale.width * 0.5, this.scale.height - 25);
this.input.once(
"pointerdown",
() => {
this.lifeLostText.visible = false;
this.ball.body.setVelocity(150, -150);
},
this,
);
} else {
alert("Game over!");
location.reload();
}
}
}

You can check the finished code for this lesson in the live demo below, and play with it to understand better how it works:
const config = {
type: Phaser.CANVAS,
width: 480,
height: 320,
scene: Example,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
},
backgroundColor: "#eee",
physics: {
default: "arcade",
},
};

const game = new Phaser.Game(config);
```

{{JSFiddleEmbed("https://jsfiddle.net/end3r/9o4pakrb/","","400")}}
{{embedlivesample("final", "", "480px")}}

## Next steps

Expand Down
Loading
Loading