Create a Breakout Game With Flame and Forge2D – Part 2

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

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Creating the Paddle

The final element of the Breakout game to make is the user-controlled paddle. Like the ball and bricks, the paddle is also a rigid body and your first step is to declare the Paddle body component.

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

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

class Paddle extends BodyComponent<Forge2dGameWorld> {
 final Size size;
 final Vector2 position;

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

 @override
 Body createBody() {
  final bodyDef = BodyDef()
   ..type = BodyType.dynamic
   ..position = position
   ..fixedRotation = true
   ..angularDamping = 1.0
   ..linearDamping = 10.0;

  final paddleBody = world.createBody(bodyDef);

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

  paddleBody.createFixture(FixtureDef(shape)
   ..density = 100.0
   ..friction = 0.0
   ..restitution = 1.0);

  return paddleBody;
 }
}

The Paddle code should be very familiar at this point. There's nothing new here — it's just another rigid body in your Forge2D world.

Now you can add the paddle to your game.

Open the file forge2d_game_world.dart then add an import for paddle.dart as well as for the size component:

import 'package:flame/extensions.dart';
import 'components/paddle.dart';

Then, create an instance of Paddle in _initializeGame just after the BrickWall:

  const paddleSize = Size(4.0, 0.8);
  final paddlePosition = Vector2(
   size.x / 2.0,
   size.y * 0.85,
  );

  final paddle = Paddle(
   size: paddleSize,
   position: paddlePosition,
  );
  await add(paddle);

You've set the paddle to four meters wide by 80 centimeters high, a reasonable size for the game area. The position is relative to the center of the paddle body. This paddlePosition centers the paddle on the x-axis and down 85% from the top of the game area.

Build and run your project. You now have all the elements for a Breakout game: a ball, a brick wall and a paddle. Woohoo!

Ball Brick Wall and Paddle

Giving User Control of the Paddle

You have your paddle, but your breakout game won’t be much fun until it responds to user input. That’s what you’ll build next.

Flame supports several input forms, including gesture input. The Flame Draggable mixin is the perfect feature for implementing user control of the paddle.

Setting Up Draggable Mixin

Open forge2d_game_world.dart and add the following import:

import 'package:flame/game.dart';

You’re including the mixin HasDraggables in your Forge2DGame to inform the game world that it’ll have draggable components.

Insert this:

class Forge2dGameWorld extends Forge2DGame with HasDraggables {

You’ve just added the HasDraggables mixin to your Forge2dGameWorld class.

Open the paddle.dart file and add:

class Paddle extends BodyComponent<Forge2dGameWorld> with Draggable {

You’ve just added the Draggable mixin to the Paddle class.

Then include the following imports to get the Draggable mixin:

import 'package:flame/components.dart';
import 'package:flame/input.dart';

And now override the mixin routine onDragUpdate, like so:

 @override
 bool onDragUpdate(DragUpdateInfo info) {
  body.setTransform(info.eventPosition.game, 0.0);

  // Don't continue passing the event.
  return false;
 }

Flame sends your draggable component’s data about the drag event so you can use it to update the paddle’s position. For now, you’re using setTransform to update the location and rotation of the paddle body.

Build and run!

To drag the paddle, you must be inside the shape area of the paddle.

Dragging the Paddle

The paddle recognizes user input but still doesn't behave how you’d expect. In this game format, it should be horizontally constrained within the game area.

in the next section, you’ll use a MouseJoint to constrain the paddle’s movement.

Constraining Body Movement with Joints

Using setTransform to define the location of a body in the Forge2d world works, but it's not the best method to move the paddle.

Why?

Because using setTransform is like being beamed from point A to point B. If points A and B are far apart, it looks unnatural—unless you live in the Star Trek universe.

It’s more natural for a body to move through a series of locations, starting a point A and ending at point B. You’ll accomplish this effect with a MouseJoint.

But a MouseJoint alone isn't enough to implement the correct Breakout paddle behavior — it must also be constrained to only move side to side.

A PrismaticJoint restricts the movement of a body along an axis.

You'll use these two joints together on the paddle body to create the desired behavior!

Note: Joints connect bodies in Forge2D. Joints are a complex topic deserving a more robust discussion, but doing so would derail you from finishing this Breakout game tutorial. There's a link at the end if you'd like to learn more.

Creating a Mouse Joint

A MouseJoint is used to make a body track to a world point.

Joints connect bodies. The paddle is one body, but what will be the second body?

The arena body fills the screen area and will make a good anchor body for the MouseJoint. The arena will be the "ground" for the MouseJoint joint.

In other words, you'll create a MouseJoint and have it track to a world point provided by DragUpdateInfo.

Open paddle.dart and add a new ground parameter to the Paddle class:

 final Size size;
 final BodyComponent ground;
 final Vector2 position;

 Paddle({
  required this.size,
  required this.ground,
  required this.position,
 });

Next, add these variables:

 MouseJoint? _mouseJoint;
 Vector2 dragStartPosition = Vector2.zero();
 Vector2 dragAccumlativePosition = Vector2.zero();

These will hold the mouse joint, the drag start position and the accumulative drag offset.

Now, you're going to change the onDragUpdate routine and add new routines for handling the start, end and cancel drag events.

 // 1
 @override
 bool onDragStart(DragStartInfo info) {
  if (_mouseJoint != null) {
   return true;
  }
  dragStartPosition = info.eventPosition.game;
  _setupDragControls();

  // Don't continue passing the event.
  return false;
 }

 // 2
 @override
 bool onDragUpdate(DragUpdateInfo info) {
  dragAccumlativePosition += info.delta.game;
  if ((dragAccumlativePosition - dragStartPosition).length > 0.1) {
   _mouseJoint?.setTarget(dragAccumlativePosition);
   dragStartPosition = dragAccumlativePosition;
  }

  // Don't continue passing the event.
  return false;
 }

 // 3
 @override
 bool onDragEnd(DragEndInfo info) {
  _resetDragControls();

  // Don't continue passing the event.
  return false;
 }

 // 4
 @override
 bool onDragCancel() {
  _resetDragControls();

  // Don't continue passing the event.
  return false;
 }

 // 5
 void _setupDragControls() {
  final mouseJointDef = MouseJointDef()
   ..bodyA = ground.body
   ..bodyB = body
   ..frequencyHz = 5.0
   ..dampingRatio = 0.9
   ..collideConnected = false
   ..maxForce = 2000.0 * body.mass;

  _mouseJoint = MouseJoint(mouseJointDef);
  world.createJoint(_mouseJoint!);
 }

 // 6
 // Clear the drag position accumulator and remove the mouse joint.
 void _resetDragControls() {
  dragAccumlativePosition = Vector2.zero();
  if (_mouseJoint != null) {
   world.destroyJoint(_mouseJoint!);
   _mouseJoint = null;
  }
 }

This code looks lengthy, but it's pretty straightforward. Here’s a step-by-step explanation:

  1. onDragStart checks to ensure there isn't already a MouseJoint in use. If not, it gets the drag start position and sets up the drag controls. Note that a mouse joint is active only during a drag event.
  2. onDragUpdate gets the current drag offset position and then checks the accumulative drag position against the paddle’s current position. The paddle position is updated only when the new position is far enough away to justify moving. Note that you removed body.setTransform from onDragUpdate and replaced it with this new code.
  3. onDragEnd resets the drag controls.
  4. onDragCancel also resets the drag controls.
  5. MouseJointDef identifies the two bodies connected by the joint and their relationship, frequencyHz is the response speed, dampingRatio is how quickly the body will stop moving, and collideConnected flags whether or not the two bodies can collide with each other. Note that this is similar to creating a body or fixture.
  6. Remove the mouse joint and reset the mouse joint variables.

Open the file forge2d_game_world.dart and update the Paddle instance, like so:

  final paddle = Paddle(
   size: paddleSize,
   ground: arena,
   position: paddlePosition,
  );
  await add(paddle);

Now your Paddle includes the new ground parameter — remember, a joint needs two bodies. The Arena is now the second body tied to the paddle.

Build and run.

Drag the paddle. You'll notice that the paddle follows the drag input. The behavior is subtle but important. Your finger doesn’t set the paddle's position; your input asks Forge2D to move the paddle to a new location.

Mouse Joint Dragging the Paddle