Create a Breakout Game With Flame and Forge2D – Part 3

Learn how to create a Flutter version of the classic Breakout game using Flame and Forge2D. By Michael Jordan.

4.6 (5) · 2 Reviews

Download materials
Save for later
Share

This article is Part 3 of a three-part tutorial on creating a Flutter Breakout game with Flame and Forge2D.

The companion articles to this tutorial are:

In Part 2 of this tutorial, you expanded your knowledge of Forge2D. You learned how to create the brick wall and paddle for your Breakout game. You also learned how to add user input controls and create joints to connect rigid bodies.

Destroying Bricks

Your game is beginning to look like the Breakout game.

Breakout Game

In this tutorial, you’ll complete your Breakout game by adding gameplay logic and skinning the game. Also, you’ll learn:

  • Add game rules and behaviors.
  • Add gameplay logic.
  • Create a Forge2D sensor.
  • Use the Flame Widgets Overlay API to add Flutter widgets to control the game.
  • Add user tap input.
  • Use Canvas to skin your game by painting the rigid BodyComponent in the game to give them color.
Note: This tutorial assumes you’re familiar with the Dart Canvas class. If you aren’t familiar with Canvas, Wilberforce Uwadiegwu’s article Flutter Canvas API: Getting Started is a great introduction.

Getting Started

You can use the project you worked on in Part 2 of this tutorial or the starter project for this tutorial. Download it by clicking the Download Materials button at the top or bottom of the tutorial.

Both of these projects have a Forge2D ball bouncing inside an arena. Also, you have a brick wall, a paddle the player can control and collision detection that removes bricks from the wall. This is the starting point for this tutorial.

Adding Game Rules and Behaviors

Games have rules and must pose a challenge to players. Unfortunately, your game at this point doesn’t have any rules and it isn’t much of a challenge — when the player misses the ball, it bounces off the bottom wall and continues. If the player destroys all the bricks, the ball continues to bounce in an empty arena. You’ll now add gameplay logic to your game.

Adding Gameplay Logic

A Breakout game is over when the player misses the ball with the paddle. Game rules also include that when a player destroys all the bricks, the player wins and the game is over. You’ll now add this gameplay logic to your game.

Open forge2d_game_world.dart and add the following enum at the top of the file before the Forge2dGameWorld class definition:

enum GameState {
 initializing,
 ready,
 running,
 paused,
 won,
 lost,
}

These will be the six states for your game. Now, add a gameState property to Forge2dGameWorld and set the initial state to initializing.

 GameState gameState = GameState.initializing;

Next, set the game state to ready once the game completes initializing. Add the following state change as the last line in _initializeGame:

 gameState = GameState.ready;

You now have the first two states of your game in place.

Winning and losing are two critical game states. First, you’ll see how to determine when the player loses the game and set the game state to GameState.lost. Then, you’ll add a check for when all the bricks in the wall are destroyed and set the game state to GameState.won.

Adding a Forge2D Sensor

You’ll now add a Forge2D sensor for the dead zone to detect when the player has missed the ball. What’s a dead zone? It’s a region at the bottom of the arena. The dead zone will use a Fixture sensor that detects collisions without generating a response. Restated, this means you can get notified of a collision, but the colliding body will pass through without responding to the collision.

Create a dead_zone.dart file in the components folder and add the following lines of code to the file:

import 'package:flame/extensions.dart';
import 'package:flame_forge2d/flame_forge2d.dart';

import '../forge2d_game_world.dart';
import 'ball.dart';

// 1
class DeadZone extends BodyComponent<Forge2dGameWorld> with ContactCallbacks {
 final Size size;
 final Vector2 position;

 DeadZone({
 required this.size,
 required this.position,
 });

 @override
 Body createBody() {
 final bodyDef = BodyDef()
 ..type = BodyType.static
 ..userData = this
 ..position = position;

 final zoneBody = world.createBody(bodyDef);

 final shape = PolygonShape()
 ..setAsBox(
 size.width / 2.0,
 size.height / 2.0,
 Vector2.zero(),
 0.0,
 );

 // 2
 zoneBody.createFixture(FixtureDef(shape)..isSensor = true);

 return zoneBody;
 }

 // 3
 @override
 void beginContact(Object other, Contact contact) {
 if (other is Ball) {
 gameRef.gameState = GameState.lost;
 }
 }
}
  1. The declaration for DeadZone body should look familiar to you. DeadZone needs to react to the ball coming into contact with it, so add the ContactCallbacks mixin.
  2. Setting the isSensor flag of the FixtureDef to true makes this body distinctive. Sensor bodies detect collisions but don’t react to them.
  3. If the ball comes into contact with the dead zone, set the gameState to GameState.lost. Forge2dGameWorld will detect the game state change in the game loop update method.

The game loop needs to check the game state and act appropriately. In this case, when the player loses, the game needs to stop. With the Flame game engine, pausing the engine is the appropriate action.

Open forge2d_game_world.dart and add these imports:

import 'package:flame/extensions.dart';

import 'components/dead_zone.dart';

Then add the DeadZone body to the _initializeGame routine between BrickWall and Paddle.

 final deadZoneSize = Size(size.x, size.y * 0.1);
 final deadZonePosition = Vector2(
 size.x / 2.0,
 size.y - (size.y * 0.1) / 2.0,
 );

 final deadZone = DeadZone(
 size: deadZoneSize,
 position: deadZonePosition,
 );
 await add(deadZone);

You want the dead zone to fill the arena area at the bottom of the screen. First, set the deadZoneSize to be the same width and 10% of the height of the game area. Next, set the deadZonePosition, so the DeadZone center is at the bottom of the game area.

Now with a dead zone in place, you can properly position the paddle. The paddle should move along the top edge of the dead zone. Change paddlePosition to place the bottom edge of the paddle at the top edge of the dead zone.

 final paddlePosition = Vector2(
 size.x / 2.0,
 size.y - deadZoneSize.height - paddleSize.height / 2.0,
 );

Add the following update routine to forge2d_game_world.dart. The update routine will listen for changes to the game state.

 @override
 void update(double dt) {
 super.update(dt);

 if (gameState == GameState.lost) {
 pauseEngine();
 }
 }

Flame calls your update routine from the game loop, allowing you to make changes or respond to events such as game state changes. Here, you’re calling pauseEngine to stop the execution of the game loop.

Build and run the project. Now, you’ll get a white rectangular area at the bottom of the screen, which is the dead zone sensor body. The game stops when the ball comes into contact with the dead zone.

Ball Contacts Dead Zone

Why is the DeadZone body white? For that matter, why are all the Forge2D bodies white? Forge2D’s BodyComponent default behavior is to render body fixture shapes, making them visible. You can turn off this default behavior by setting the renderBody property of a BodyComponent to false.

Open dead_zone.dart and add the following line of code at the top of the DeadZone class after the constructor.

 @override
 bool get renderBody => false;

Build and run the project. The dead zone body remains, but Forge2D is not rendering the fixture shapes on the body. In an upcoming section, you’ll learn more about rendering bodies when you “skin” the game.

Invisible Dead Zone