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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Create a Breakout Game With Flame and Forge2D – Part 3
30 mins
- Getting Started
- Adding Game Rules and Behaviors
- Adding Gameplay Logic
- Adding a Forge2D Sensor
- Adding the Win Game State
- Adding Start and Reset Controls
- Flame Overlays
- Adding a Game-Ready Overlay
- Adding Player Tap Input
- Adding a Game-Over Overlay
- Resetting the Game
- Skinning Your Game
- Rendering the Ball
- Rendering the Paddle
- Rendering the Brick Wall
- Where to Go From Here?
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.
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.
- You can override the Forge2D
BodyComponent
render method to customize drawing the body. Therender
method passes you a reference to the DartCanvas
, where you can draw the body. - The ball body has a single
CircleShape
fixture. Get the shape information from the body. - Create a
Paint
object with the gradient to use when drawing the ball. - Draw the ball with the radial gradient.
Build and run the project. Notice the shading effect on the ball? Pretty cool, huh?
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.
That leaves coloring the brick wall.