Introduction to Phaser 3: Building Breakout

Introduction

Game development is a unique branch of software development that can be as rewarding as it is complex. When thinking of creating games, we usually think of an application to install and play on our computers or consoles.

The HTML5 spec introduced many APIs to enable game development on the web, allowing our games to reach many users on different computing devices. Phaser is a popular game framework that enables us to quickly build games for the web.

The best way to master game development is to make games. We'll use Phaser to create a Breakout clone - a version of the classic and everlasting Atari game released in 1976.

This tutorial contains some very basic HTML and CSS. You will need to be comfortable with JavaScript functions and objects. It also makes light use of ES2015 features.

The Game Loop

All games run within a loop. After setting up our game world, we enter the game loop which performs the following tasks:

  1. Processes input
  2. Updates the game world
  3. Renders the changes

Let's see how the game loop works in a game like Megaman. After sifting through the menu to start a level, the game decides where to place platforms and loads the music to be played. That setup usually happens during the loading screen.

When the game starts you are in control of Megaman in a world with platforms, enemies, and a particular song for that level. You can use your joystick to move Megaman, and press a button to jump or shoot. The game loop is processing the input, updating the position of Megaman and rendering those changes many times in a second.

What is Phaser?

Phaser is a HTML5 game framework. It uses many HTML5 APIs like Canvas, WebGL, Audio, Gamepad, etc. and adds some helpful logic like managing the game loop and providing us with a physics engines.

With Phaser, we can build 2D games with nothing but HTML, CSS, and JavaScript.

Breakout Rules

Before we use Phaser to build our Breakout clone, let's first define the scope of the game:

  • This single player game has one level with 30 bricks, a paddle, and a ball
  • The goal is to get the ball to destroy every brick, while ensuring it does not leave the bottom of the game screen.
  • The player will control a paddle, which can move left and right
  • The game is built for Desktop web users, so the keyboard will be used for input

Setting up Phaser

Phaser is a JavaScript library, to develop and play our game we'll need some basic HTML to load the JS. Create a directory called breakout in one of your workspaces.

Create the following files and folders in your directory:

  • An index.html file
  • A breakout.js file
  • A folder called assets
  • Within your assets folder, create an images folder

Game assets are art, sound, video, and other data used by the game. For this simple Breakout clone, there aren't many assets that necessitate organizing with folders. However, it is good practice to keep your assets separate from your code and to separate your assets by their type.

Add the following code to your index.html file:

<!doctype html>  
<html>

<head>  
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
  <title>Breakout</title>
  <style>
    html,
    body {
      margin: 0 auto;
      padding: 0;
      width: 100%;
      height: 100%;
    }

    #game {
      margin: 10px auto;
      padding: 0;
      width: 800px;
      height: 640px;
    }
  </style>
</head>

<body>  
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="game"></div>
  <script src="//cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  <script src="breakout.js"></script>
</body>

</html>  

This basic HTML code does the following:

  • Removes browser margins and padding from HTML and body tag
  • Adds a game div element which will contain our Breakout clone
  • Loads Phaser v3.17 via their CDN
  • Loads our breakout.js file which currently does nothing but will contain our game logic

To effectively develop games with Phaser we'll need these files to be served by a web server. Without a web server, our browser won't allow our game script to load our assets for security reasons.

Luckily there's no need to set up Apache or Nginx to get a running web server. If you use VisualStudio Code like me then you can install the Live Server extension. Most IDEs and text editors have a plugin with similar functionality.

If you have Python version 3 installed you can go to your workspace via the terminal and enter python3 -m http.server. There are other CLI tools which provide simple web servers, choose the one that gives you the quickest time to develop your game.

Lastly, download the image assets we've created for this game from this link. Copy and paste the PNG files into the images folder.

Development tip - when you're developing a game you probably want to have the JavaScript console visible so you can see any errors that pop up. If you're using Chrome or Firefox, right click on the page and select "Inspect Element". A box should pop up from the bottom or side of your browser window. Select the "Console" tab to see updates errors or logs from our JavaScript code.

Creating our Game World

With our HTML and CSS set up, let's edit our breakout.js file to set up our game world.

Starting Phaser

First, we need to configure Phaser and create our Game instance. The Game instance is the central controller for a Phaser game, it does all the setup and kick starts the game loop for us.

Let's configure and create our Game instance:

// This object contains all the Phaser configurations to load our game
const config = {  
  type: Phaser.AUTO,
  parent: 'game',
  width: 800,
  heigth: 640,
  scale: {
    mode: Phaser.Scale.RESIZE,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },
  scene: {
    preload,
    create,
    update,
  },
  physics: {
    default: 'arcade',
    arcade: {
      gravity: false
    },
  }
};

// Create the game instance
const game = new Phaser.Game(config);  

The type property tells Phaser what renderer to use. Phaser can render our game using HTML5's WebGL or Canvas element. By setting the type to Phaser.AUTO, we're telling Phaser to first try rendering with WebGL and if that fails, render using Canvas.

The parent property indicates the ID of the HTML element our game will be played in. We define our game dimensions in pixels with width and height. The scale object does two things for us:

  • mode tells Phaser how to use the space of our parent element, in this case, we ensure that the game fits the size of the parent div
  • autoCenter tells Phaser how to center our game within our parent div if we want to. In this case, we center our game vertically and horizontally within the parent div. This property is more useful when the game does not take up the entire space of the parent div, it's shown here as it's a frequently asked question.

In Phaser, our game logic is defined in Scenes. Think of scenes as various states in our game: the title screen is one scene, each level of a game would be their own scene, a cut scene would be its own scene, etc. Phaser provides a Scene object but it can also work with a regular JavaScript object containing the preload(), create() and update() functions defined.

The last configuration tells Phaser which physics engine to use. Phaser can use 3 different physic engines: Arcade, Impact and Matter. Arcade is the simplest one to get started with and is sufficient for our game needs.

Breakout does not need gravity to work, so we disable the property in our physics engine. If we were building a platformer we would probably enable gravity, so that when our players jump they will fall naturally back to the ground.

To ensure our game set up works we need to add the preload(), create() and update() functions. Add the following blank functions to after creating our game instance:

function preload() { }

function create() { }

function update() { }  

With your web server running, navigate to the page where your game is running. You should see a blank screen like this:

Game Setup

Loading Assets

The assets in this game consist of 5 images. In other games you may create, your assets can be huge. High definition images, audio, and video files could take up megabytes of space. The larger the asset, the longer it takes the load. For that reason, Phaser has a preload() function where we can load all the assets before we start running the game. It's never a nice user-experience to be playing a game and it suddenly slows down because it's trying to load new assets.

Change the preload() function to the following so we can load our images before the game loop kicks off:

function preload() {  
  this.load.image('ball', 'assets/images/ball_32_32.png');
  this.load.image('paddle', 'assets/images/paddle_128_32.png');
  this.load.image('brick1', 'assets/images/brick1_64_32.png');
  this.load.image('brick2', 'assets/images/brick2_64_32.png');
  this.load.image('brick3', 'assets/images/brick3_64_32.png');
}

The first argument is the key we'll use later to reference the image, the second argument is the location of the image.

Note: - when we use this in our preload(), create(), and update() functions, we are referring to the scene run by the game instance that was created earlier.

With the images loaded, we want to place sprites on the screen. At the top of the breakout.js, add these variables that will contain our sprite data:

let player, ball, violetBricks, yellowBricks, redBricks, cursors;  

Once they are globally defined, all our functions are able to use them.

Adding Sprites

A sprite is any 2D image that's part of a game scene. In Phaser, a sprite encapsulates an image along with its position, velocity, physical properties, and other properties. Let's begin by creating our player sprite via the create() function:

player = this.physics.add.sprite(  
  400, // x position
  600, // y position
  'paddle', // key of image for the sprite
);

You should now be able to see a paddle on the screen:

Player on screen

The first argument of the sprite() method is the X Coordinate to place the sprite. The second argument is the Y Coordinate, and the last argument is key to the image asset added in the preload() function.

It's important to understand how Phaser and most 2D game frameworks use coordinates. The graphs we learned in school usually place the origin i.e. point (0, 0) at the center. In Phaser, the origin is at the top left of the screen. As x increases, we are essentially moving to the right. As y increases, we are moving downward.

Our game has a width of 800 pixels and a height of 640 pixels, so our game coordinates would look like this:

Game coordinates

Let's add the ball to sit above the player. Add the following code to the create() function:

ball = this.physics.add.sprite(  
  400, // x position
  565, // y position
  'ball' // key of image for the sprite
);

As the ball is above our player, the value of the Y Coordinate is lower than the player's Y Coordinate.

Adding Sprite Groups

While Phaser makes it easy to add sprites, it would quickly become tedious if every sprite would have to be defined individually. The bricks in Breakout are pretty much identical. The positions are different, but their properties, like color and how they interact with the ball, are the same. Instead of creating 30 brick sprite objects, we can create sprite groups to better manage them.

Let's add the first row of violet bricks via the create() function:

// Add violet bricks
violetBricks = this.physics.add.group({  
  key: 'brick1',
  repeat: 9,
  setXY: {
    x: 80,
    y: 140,
    stepX: 70
  }
});

Instead of this.physics.add.sprite() we use this.physics.add.group() and pass a JavaScript object. The key property references the image key that all the sprites in the sprite group will use. The repeat property tells Phaser how many more times to create a sprite. Every sprite group creates one sprite. With repeat set to 9, Phaser will create 10 sprites in that sprite group. The setXY object has three interesting properties:

  • x is the X Coordinate of the first sprite
  • y is the Y Coordinate of the second sprite
  • stepX is the length in pixels between repeated sprites on the x-axis.

There is a stepY property as well but we don't need to use it for this game. Let's add the two other remaining sprite group for bricks:

// Add yellow bricks
yellowBricks = this.physics.add.group({  
  key: 'brick2',
  repeat: 9,
  setXY: {
    x: 80,
    y: 90,
    stepX: 70
  }
});

// Add red bricks
redBricks = this.physics.add.group({  
  key: 'brick3',
  repeat: 9,
  setXY: {
    x: 80,
    y: 40,
    stepX: 70
  }
});

Our game is already coming together, your screen should look like this:

All Sprites Added

Winning and Losing

It's good game development (and programming) practice to keep the end in sight. In Breakout, we can lose a game if our ball falls to the bottom of the screen. In Phaser, for the ball to be below the screen then the Y Coordinate of the ball is greater than the height of the game world. Let's create a function which checks this, add the bottom of the breakout.js add the following:

function isGameOver(world) {  
  return ball.body.y > world.bounds.height;
}

Our function takes the world object from the scene's physics property, which will be available in the update() function. It checks if the Y-coordinate of the ball sprite is greater than the height of the game world boundaries.

To win the game we need to get rid of all the bricks. Sprites in Phaser all have an active property. We can use that property to determine if we won or not. Sprite groups can count the number of active sprites contained within them. If there are no active sprites in each of the brick sprite groups i.e. there are 0 active brick sprites, then the player won the game.

Let's update the breakout.js file, by adding a check at the bottom:

function isWon() {  
  return violetBricks.countActive() + yellowBricks.countActive() + redBricks.countActive() === 0;
}

We accept each of the sprite groups as parameters, add the number of active sprites within them and check if it's equal to 0.

Now that we have defined our winning and losing conditions, we want Phaser to check them at the beginning of the game loop. As soon as the player wins or loses, the game should stop.

Let's update the update() function:

function update() {  
  // Check if the ball left the scene i.e. game over
  if (isGameOver(this.physics.world)) {
    // TODO: Show "Game over" message to the player
  } else if (isWon()) {
    // TODO: Show "You won!" message to the player
  } else {
    // TODO: Logic for regular game time
  }
}

Moving the Player with Keyboard Input

The player's movement depends on keyboard input. To be able to track keyboard input. It's time to use the cursors variable.

And at the bottom of our create() function:

cursors = this.input.keyboard.createCursorKeys();  

Cursor keys in Phaser track the usage of 6 keyboard keys: up, right, down, left, shift and space.

Now we need to react to the state of our cursors object to update the position of our player. In the else clause of our update() function add the following:

// Put this in so that the player stays still if no key is being pressed
player.body.setVelocityX(0);

if (cursors.left.isDown) {  
  player.body.setVelocityX(-350);
} else if (cursors.right.isDown) {
  player.body.setVelocityX(350);
}

Now we can move our player left to right!

Player moving

You would notice that the player sprite is able to leave the game screen, ideally, it should not. We'll address that later when we handle collisions.

Waiting to Start

Before we add logic to move the ball, it would help if the game waited on user input before it moved. It's not a good experience to load a game and be immediately forced start. The player wouldn't have a fair time to react!

Let's move the ball upward after the player presses the spacebar. If the user moves the paddle to the left or the right, the ball will be moved as well so that's it's always at the center of the paddle.

First, we need our own variable to track whether a game was started or not. At the top of the breakout.js, after the declaration of our game variables, let's add:

let gameStarted = false;  

Now, in the else clause of our update function:

if (!gameStarted) {  
  ball.setX(player.x);

  if (cursors.space.isDown) {
    gameStarted = true;
    ball.setVelocityY(-200);
  }
}

If the game has not started, set the X-coordinate of our ball to the player's center. A game object's coordinates is based on their center, so the x and y properties of sprites relate point to the center of our sprites.

Note that while it's fine to get a property's value like x by referencing it directly when setting properties we always try to use the appropriate setter function. The setter functions can include logic to validate our input, update another property or fire an event. It makes our code more predictable.

Like before with moving the player, we check if the space bar was pressed. If it was pressed, we switch the gameStarted flag to true so the ball would no longer follow the player's horizontal position, and set the Y-velocity of the ball to -200. Negative y-velocities send objects upward. For positive velocities, larger values move objects downward quicker. For negative velocities, smaller values move objects upward quicker.

Now when we move the player, the ball follows and if we press the spacebar the ball shoots upward:

Press to Start

You would observe a few things from how our game behaves so far:

  1. The ball is rendered behind the bricks
  2. The player can leave the bounds of the screen
  3. The ball can leave the bounds of the screen

The ball is rendered behind the bricks because it was added to the game in our create function before the brick sprite groups. In Phaser, and generally with the HTML5 canvas element, the most recently added image is drawn on top of previous images.

The last two issues can be solved by adding some world collision.

Handling Collisions

World Collision

All our sprite interactions are defined in the create function. Enabling collision with the world scene is very easy with Phaser, add the following to the end of the create function:

player.setCollideWorldBounds(true);  
ball.setCollideWorldBounds(true);  

It should give us output like this:

World Collision

While the player movement is fine, the ball seems stuck at the top. To rectify this, we need to set the bounce property of the ball sprite. The bounce property would tell Phaser how much velocity to maintain after colliding with an object.

Add this to end of your create() function:

ball.setBounce(1, 1);  

This tells phaser that the ball should maintain all of its X and Y-velocity. If we release the ball with the spacebar the ball should be bouncing up and down the game world. We need to disable the collision detection from the bottom part of the game world.

If we don't, we'll never know when it's game over. Disable collision with the bottom of the game world by adding this line at the end of the create function:

this.physics.world.checkCollision.down = false;  

We should now have a game like this:

Ball falls to the bottom of game world

Brick Collision

Now that our moving sprites correctly collide with our game world, let's work on the collision between the ball and the bricks and then the ball and the player.

In our create() function add the following lines of code to the end:

this.physics.add.collider(ball, violetBricks, hitBrick, null, this);  
this.physics.add.collider(ball, yellowBricks, hitBrick, null, this);  
this.physics.add.collider(ball, redBricks, hitBrick, null, this);  

The collider method tells Phaser's physics system to execute the hitBrick() function when the ball collides with various brick sprite groups.

Every time we press the space bar, the ball shoots upward. There's no X-velocity so the ball would come straight back to the paddle. That would be a boring game. Therefore, when we first hit a brick we'll set a random X-velocity.

At the bottom of the breakout.js define hitBrick below:

function hitBrick(ball, brick) {  
  brick.disableBody(true, true);

  if (ball.body.velocity.x === 0) {
    randNum = Math.random();
    if (randNum >= 0.5) {
      ball.body.setVelocityX(150);
    } else {
      ball.body.setVelocityX(-150);
    }
  }
}

The hitBrick() function accepts the previous two arguments that were used in the collider() method, for example, ball and violetBricks. The disableBody(true, true) call on the brick tells Phaser to make it inactive and to hide it from the screen. If the X-velocity of the ball is 0, then we give the ball a velocity depending on the value of a random number.

If a small ball rolls towards your foot at a slow pace, on collision it would come to a halt. The Arcade Physics engine models the impact collision has on velocity by default. For our game, we don't want the ball to lose velocity when it hits a brick. We need to set the immovable property to our sprite groups to true.

Update the definitions of violetBricks, yellowBricks and redBricks to the following:

// Add violet bricks
violetBricks = this.physics.add.group({  
  key: 'brick1',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 140,
    stepX: 70
  }
});

// Add yellow bricks
yellowBricks = this.physics.add.group({  
  key: 'brick2',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 90,
    stepX: 70
  }
});

// Add red bricks
redBricks = this.physics.add.group({  
  key: 'brick3',
  repeat: 9,
  immovable: true,
  setXY: {
    x: 80,
    y: 40,
    stepX: 70
  }
});

Our brick collision is now complete and our game should work like this:

Brick Collision

Development tip - when developing the physics of your game, you may want to enable debug mode to see your sprites' boundary boxes and how they collide with one another. In your game config object, within the arcade property where we defined gravity, you can enable debugging by adding this to the object:

debug: true  

Player Collision

Managing collisions between the ball and player are a similar endeavor. First, let's ensure that the player is immovable. At the end of the create() function add the following:

player.setImmovable(true);  

And then we add a collider between the ball and player:

this.physics.add.collider(ball, player, hitPlayer, null, this);  

When the ball hits the player, we want two things to happen:

  • The ball should move a bit faster, to gradually increase the difficulty of the game
  • The ball's horizontal direction depends on which side of the player it hit - if the ball hits the left side of the player then it should go left, if it hits the right side of the player then it should go to the right.

To accommodate for these, let's update breakout.js with the hitPlayer() function:

function hitPlayer(ball, player) {  
  // Increase the velocity of the ball after it bounces
  ball.setVelocityY(ball.body.velocity.y - 5);

  let newXVelocity = Math.abs(ball.body.velocity.x) + 5;
  // If the ball is to the left of the player, ensure the X-velocity is negative
  if (ball.x < player.x) {
    ball.setVelocityX(-newXVelocity);
  } else {
    ball.setVelocityX(newXVelocity);
  }
}

Note: A sprite can collide with another sprite, a sprite can collide with a sprite group, and sprite groups can collide with each other. Phaser is smart enough to use the collision function we define appropriate in the context.

Now our game has both player and brick collision:

Player collision

Adding Text

While our game is fully functional, someone playing this game would have no idea how to start or know when the game is over.

Let's add 3 new global variables that will store our text data after the gameStarted declaration at the top of the breakout.js:

let openingText, gameOverText, playerWonText;  

Opening Text

Let's add some text when the game is loaded to tell the player to press space. In the create() function add the following code:

openingText = this.add.text(  
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'Press SPACE to Start',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  }
);

openingText.setOrigin(0.5);  

The first two arguments of the text method are the X and Y-coordinates of the text box. We use the game scene's width and height to determine where it's placed - in the center. The third argument is the text to display. The fourth argument is a JS object that contains font related data.

Unlike sprites, text objects X and Y-coordinates refer to their top-left most point of the object, not their center. That's why we use the setOrigin() method to make the coordinates system work like sprites, in this case, it makes it easier to position in the center.

When we are playing, we don't want to see the opening text anymore. In the update() function, change the if statement that checks if the space bar was pressed to the following:

if (cursors.space.isDown) {  
  gameStarted = true;
  ball.setVelocityY(-200);
  openingText.setVisible(false);
}

Text objects are not sprites, we can't disable their bodies. We can make them invisible when we don't need to see them. Our game now begins like this:

Opening text starts game

Game Over and Game Won

Like before we need to add the text objects in the create() function, and make them invisible so they won't be seen when the game is started:

// Create game over text
gameOverText = this.add.text(  
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'Game Over',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  }
);

gameOverText.setOrigin(0.5);

// Make it invisible until the player loses
gameOverText.setVisible(false);

// Create the game won text
playerWonText = this.add.text(  
  this.physics.world.bounds.width / 2,
  this.physics.world.bounds.height / 2,
  'You won!',
  {
    fontFamily: 'Monaco, Courier, monospace',
    fontSize: '50px',
    fill: '#fff'
  }
);

playerWonText.setOrigin(0.5);

// Make it invisible until the player wins
playerWonText.setVisible(false);  

Now they're defined, we have to change their visibility in the update() function:

// Check if the ball left the scene i.e. game over
if (isGameOver(this.physics.world)) {  
  gameOverText.setVisible(true);
  ball.disableBody(true, true);
} else if (isWon()) {
  playerWonText.setVisible(true);
  ball.disableBody(true, true);
} else {
  ...
}

We disable the ball's body so it would stop being updated and displayed as it's no longer needed.

If we lose the game, we'll see this:

Game Over

If we win the game, we'll see this:

Game Won

Our Breakout clone is complete!

Conclusion

Phaser is an HTML5 game development framework that allows us to quickly build video games on the web. Aside from abstracting over the HTML5 APIs, it also provides us with useful utilities like physics engines and manages the game loop - the execution lifecycle of all games.

The best way to improve your game development skills is to keep building games. If you would like to learn more about game development with Phaser, then have a look at the official website's introductory tutorial.

You can view the annotated source code for the game here.

Author image
Trinidad and Tobago Twitter Website
Web Dev|Games|Music|Art|Fun|Caribbean I love many things and coding is one of them!