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
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Resetting the Game

You now have a postgame overlay, but you must make your game resettable.

First, open forge2d_game_world.dart and make all the Forge2D bodies instance variables. These will be late final variables because the bodies aren’t created until the game is loading.

 late final Arena _arena;
 late final Paddle _paddle;
 late final DeadZone _deadZone;
 late final BrickWall _brickWall;

After you’ve created the instance variables, fix the variable initializations in _initializeGame.

 Future<void> _initializeGame() async {
 _arena = Arena();
 await add(_arena);

 final brickWallPosition = Vector2(0.0, size.y * 0.075);

 _brickWall = BrickWall(
 position: brickWallPosition,
 rows: 8,
 columns: 6,
 );
 await add(_brickWall);

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

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

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

 _paddle = Paddle(
 size: paddleSize,
 ground: _arena,
 position: paddlePosition,
 );
 await add(_paddle);

 final ballPosition = Vector2(size.x / 2.0, size.y / 2.0 + 10.0);

 _ball = Ball(
 radius: 0.5,
 position: ballPosition,
 );
 await add(_ball);

 gameState = GameState.ready;
 overlays.add('PreGame');
 }

Now, make the three Breakout game components — the ball, paddle and wall — resettable.

Open ball.dart and add the following reset method:

 void reset() {
 body.setTransform(position, angle);
 body.angularVelocity = 0.0;
 body.linearVelocity = Vector2.zero();
 }

In the reset method, you’re resetting the ball’s location back to its initial position and setting the angular and linear velocities to zero, a ball at rest.

Now, open paddle.dart and add this reset method:

 void reset() {
 body.setTransform(position, angle);
 body.angularVelocity = 0.0;
 body.linearVelocity = Vector2.zero();
 }

Finally, open brick_wall.dart and add this reset method:

 Future<void> reset() async {
 removeAll(children);
 await _buildWall();
 }

Now, open forge2d_game_world.dart. First, add a call to show the postgame overlay when the game state is lost or won, inside the update function:

 if (gameState == GameState.lost || gameState == GameState.won) {
 pauseEngine();
 overlays.add('PostGame');
 }

Then, add the following code to resetGame.

 Future<void> resetGame() async {
 gameState = GameState.initializing;

 _ball.reset();
 _paddle.reset();
 await _brickWall.reset();

 gameState = GameState.ready;

 overlays.remove(overlays.activeOverlays.first);
 overlays.add('PreGame');

 resumeEngine();
 }

This method sets the game state to initializing and then calls the reset methods on the three dynamic components. After the game components reset, set the game state to ready, replace the postgame overlay with the pregame overlay and resume the game.

Now, open main_game_page.dart and add the postgame overlay to the overlayBuilderMap.

 overlayBuilderMap: const {
 'PreGame': OverlayBuilder.preGame,
 'PostGame': OverlayBuilder.postGame,
 },

Build and run the project. The game now congratulates the player for winning or the game is over. In both cases, the player can press a button to replay the game.

PostGame Overlay

Tip: Testing the win-game state can be tedious, if you have to destroy all the bricks. To make winning the game easier, set the rows and columns of the brick wall to a smaller value.

Congratulations! You have a functional Breakout game.

Your game has the needed components and functionality for a Breakout game. You’ve added gameplay logic for winning and losing a game. You’ve added game states to control setting up, playing and resetting the game. But, something’s missing. The game isn’t beautiful.

You must “skin” your game to make it pretty.

Skinning Your Game

Several methods can make your game prettier. Flame supports Sprites and other tools to skin games. Also, Forge2D’s BodyComponent has a render method you can override to provide your custom render method. In the following sections, you’ll learn to create a custom render method for the ball, paddle and brick wall.

Rendering the Ball

Forge2D is two-dimensional. A ball is a three-dimensional sphere. So what can you do to give the ball a 3D look? Gradients! Rendering the ball with a radial gradient will provide the 3D illusion needed.

Open ball.dart and add the following imports:

import 'package:flutter/rendering.dart';

import 'package:flame/extensions.dart';

Now, add the following gradient code after the Ball constructor:

 final _gradient = RadialGradient(
 center: Alignment.topLeft,
 colors: [
 const HSLColor.fromAHSL(1.0, 0.0, 0.0, 1.0).toColor(),
 const HSLColor.fromAHSL(1.0, 0.0, 0.0, 0.9).toColor(),
 const HSLColor.fromAHSL(1.0, 0.0, 0.0, 0.4).toColor(),
 ],
 stops: const [0.0, 0.5, 1.0],
 radius: 0.95,
 );

Using HSL, hue, saturation and light, color declarations can be easier to read and understand than other color models. These three colors are shades of white at 100%, 90% and 40% lightness. This RadialGradient uses these shades of white to give the ball a cue-ball appearance.

Next, add the following render method to the Ball component:

 //1
 @override
 void render(Canvas canvas) {

 // 2
 final circle = body.fixtures.first.shape as CircleShape;

 // 3
 final paint = Paint()
 ..shader = _gradient.createShader(Rect.fromCircle(
 center: circle.position.toOffset(),
 radius: radius,
 ))
 ..style = PaintingStyle.fill;

 // 4
 canvas.drawCircle(circle.position.toOffset(), radius, paint);
 }

The render method is simple. Let’s take a closer look.

  1. You can override the Forge2D BodyComponent render method to customize drawing the body. The render method passes you a reference to the Dart Canvas, where you can draw the body.
  2. The ball body has a single CircleShape fixture. Get the shape information from the body.
  3. Create a Paint object with the gradient to use when drawing the ball.
  4. Draw the ball with the radial gradient.

Build and run the project. Notice the shading effect on the ball? Pretty cool, huh?

Rendered Ball

Rendering the Paddle

Rendering the paddle is like how you rendered the ball, but easier. To paint the paddle, you’ll use a single opaque color.

Open paddle.dart and add the following imports:

import 'package:flutter/rendering.dart';

Then add the following render method to the Paddle component:

 @override
 void render(Canvas canvas) {
 final shape = body.fixtures.first.shape as PolygonShape;

 final paint = Paint()
 ..color = const Color.fromARGB(255, 80, 80, 228)
 ..style = PaintingStyle.fill;

 canvas.drawRect(
 Rect.fromLTRB(
 shape.vertices[0].x,
 shape.vertices[0].y,
 shape.vertices[2].x,
 shape.vertices[2].y,
 ),
 paint);
 }

The PolygonShape has the vertices of the paddle in shape.vertices. The first point is the upper left-hand corner of the rectangle. The lower right-hand corner is the third point. You can use these points to draw the paddle on the canvas.

Build and run the project. You’ve colorized the paddle.

Rendered Paddle

That leaves coloring the brick wall.