Create A Breakout Game in Flutter With Flame and Forge2D – Part 1
[ad_1]
As Flutter continues to mature and expand its capabilities on multiple platforms, it’s also branching out to embrace new software domains like game development. As a result, more indie developers are jumping on the bandwagon and creating great games using Flutter.
You have several options for building a game in Flutter. Your choice will largely depend on the type of game you want to create. For example, Filip Hráček created a tic-tac-toe game using just Flutter widgets. Vincenzo Guzzi built a 2D orthographic game in his excellent article Building Games in Flutter with Flame: Getting Started using Flame.
This is the first of three articles that show you how to build a version of the classic game Breakout using Flame and Forge2D, a two-dimensional physics simulator engine for games.
Here’s what you’ll learn in each part:
- In Part 1, you’ll learn the basics of creating a Forge2D game using Flutter and Flame. Then, you’ll learn how to set up the game loop and create rigid bodies in a simulated two-dimensional physical world. By the end of the article, you’ll have created a Forge2D game with a ball that ricochets off the walls of a contained area.
- In Part 2, you’ll continue building the remaining components for your Breakout game. You’ll learn how to build a brick wall and a user-controlled paddle. By the end of the article, you’ll have all the essential elements for your Breakout game.
- And finally, in Part 3, you’ll learn how to add gameplay logic and skin your game with the visual, completing the look and feel for a playable Breakout game.
Getting Started
Building a Breakout game in Forge2D is a big enough challenge that it makes sense to tackle the task in three parts. In the first part of your journey, you’ll learn how to:
- Create a Flame
GameWidget
with aForge2DGame
child widget. - Create Bodies and Fixtures, the component building blocks of a Forge2D world.
- Work with Forge2D world coordinates and learn how they relate to Flutter’s logical pixels.
- Learn about the Flame
Camera
and viewing into the Forge2D world.
You’ll need the starter project to complete this tutorial. Download it by clicking the Download Materials button at the top or bottom of the tutorial.
Open the project in your preferred IDE. This tutorial used Visual Studio Code, but any Flutter development environment should work. Next, open pubspec.yaml and get the project dependencies, then build and run the project.
You’ll see a green border and the text Flame Game World Goes Here! centered on the display.
The screen images in this tutorial are from the iOS Simulator, but the app will run and look similar on Android or a Chrome browser.
Take a moment to familiarize yourself with the starter project. This project is a minimal Flutter app with a simple lib/main.dart implementation that creates an instance of the MainGamePage
widget. Look at the MainGameState
widget class in lib/ui/main_game_page.dart, and you’ll see a Scaffold
widget with a Container
widget for the body. In this tutorial, you’ll replace the Container
‘s child widget, the Center
widget, with a Flame GameWidget
. The GameWidget
will contain the Forge2D world of your Breakout game.
Breakout Game Requirements
The game’s objective is simple — destroy all the bricks in the wall by repeatedly bouncing the ball off a paddle. Each time the ball hits a brick, that brick is destroyed. Eliminate all bricks, and you win the game. Miss the ball, and you lose the game.
Breakout consists of three components:
- A ball in motion.
- A user-controlled paddle.
- A wall of bricks.
To create the game, you need to draw a ball on-screen and update its position in a way that simulates the motion of a ball in the real world. Then, you’ll need to detect when the ball comes into contact with a brick, the paddle or the sides of the game area, then have the ball bounce off them as a ball would in the real world. Players expect the ball’s behavior in Breakout to mimic real-world examples like tennis or handball. Otherwise, its behavior would be confusing or unexpected to the player.
While you can create a Breakout game using Dart and Flame alone, you would have to perform all the calculations for the physical interactions between the ball, the paddle and the bricks. That’s a lot of work! Here’s where Forge2D comes to the rescue.
Understanding the Flame Game Engine and Forge2D
Forge2D is a two-dimensional physics simulator specifically designed for games. Forge2D integrates with the Flame game engine to work with Flame’s game loop to update and render objects while obeying Newton’s three laws of motion. So, you can create the ball, paddle and a wall of bricks in Forge2D and then let it do all the heavy lifting.
Adding Flame and Forge2D Dependencies
Begin by opening the pubspec.yaml file in your project, and add the flame and flame_forge2D packages:
dependencies:
flame: ^1.4.0
flame_forge2d: ^0.12.3
flutter:
sdk: flutter
Save pubspec.yaml, and run flutter pub get to get the packages.
Setting up Your Flame Game Loop
The first step in creating your game is to make a Flame game loop. The game loop is the core component, the pulsing heart of your game. You’ll create and manage all your game components from here.
Open your lib folder, and create a file called forge2d_game_world.dart. Then, add a new class named Forge2dGameWorld
to this file. Your game will extend the base Forge2D game widget Forge2DGame
:
import 'package:flame_forge2d/flame_forge2d.dart';
class Forge2dGameWorld extends Forge2DGame {
@override
Future<void> onLoad() async {
// empty
}
}
Note: A typical Flame game extends the FlameGame
class to get the Flame game loop and other core Flame properties and behaviors. A Forge2D game similarly extends Forge2DGame
. Forge2DGame
extends FlameGame
to provide Forge2D features in addition to those in FlameGame
for your game.
Next, open main_game_page.dart, and add these two imports with the other import statement at the top of the file:
import 'package:flame/game.dart';
import '../forge2d_game_world.dart';
Then, in the same file, create an instance of your new game loop class, replacing the comment // TODO: Create instance of Forge2dGameWorld here
.
final forge2dGameWorld = Forge2dGameWorld();
build
method will cause your game to rebuild every time the Flutter tree gets rebuilt, which usually is more often than you’d like.
Now, replace the Center
widget below the comment // TODO: Replace Center widget with GameWidget
with a GameWidget
and your forge2dGameWorld
instance:
child: GameWidget(
game: forge2dGameWorld,
),
Build and run your project. Now, you’ll see the familiar green border around a black rectangle, but the text is gone. The centered Text
widget has been replaced with your Flame GameWidget
, waiting for you to add game components.
Creating the Ball
FlameGame
, and by extension, Forge2DGame
, is a component-based game framework. It manages a tree of components, similar to how Flutter manages a tree of widgets. The game loop repeatedly calls the update
and render
methods of the components you add to your game, allowing you to interact with components and add game logic.
To create a ball, you need to describe the physical properties of the ball as a rigid body for Forge2D and wrap it in a component for Flame to manage. You’ll provide this description by declaring a Ball
class that extends from a BodyComponent
.
Defining a Ball’s Physical Properties
Bodies are the fundamental objects in the physics scene. They hold a rigid body’s physical properties. There are three types of bodies in Forge2D: static, dynamic and kinematic:
- Static bodies don’t move. The bricks in the brick wall will be static bodies.
- Dynamic bodies react to forces. Forge2D updates dynamic bodies while obeying Newton’s laws of motion. The ball and paddle are dynamic bodies.
- Kinematic bodies are a hybrid between static and dynamic bodies. A Ferris wheel is an example of a kinematic body. The Ferris wheel position remains fixed, but the motion of the Ferris wheel rotating around its center is dynamic. The Breakout game doesn’t use kinematic bodies.
Fixtures are the shape of a body. Forge2D uses fixtures to determine collisions between bodies. Bodies can have zero or more fixtures. A body with no fixtures is relatively meaningless, as fixtures give the body a physical presence in the Forge2D world. Fixtures have a shape and density, thus providing mass to the body. For example, the ball in your game will have a single circular shape fixture. So why would a body have multiple fixtures? Consider a fan with four blades. A fan body would have four fixtures, a polygon shape for each fan blade positioned at 90-degree intervals around the body’s center.
Create a new folder named components in the lib folder. You’ll keep your game components files in this folder. Then, create a ball.dart file in this folder, and add the following lines of code to this file:
import 'package:flame_forge2d/flame_forge2d.dart';
import '../forge2d_game_world.dart';
// 1
class Ball extends BodyComponent<Forge2dGameWorld> {
// 2
final Vector2 position;
final double radius;
Ball({required this.position, required this.radius});
// 3
@override
Body createBody() {
// 4
final bodyDef = BodyDef()
..type = BodyType.dynamic
..position = position;
// 5
final ball = world.createBody(bodyDef);
// 6
final shape = CircleShape()..radius = radius;
// 7
final fixtureDef = FixtureDef(shape);
// 8
ball.createFixture(fixtureDef);
return ball;
}
}
Going through this step-by-step:
- You begin by declaring your
Ball
to be aBodyComponent
, a rigid body in Forge2D and a component for Flame. Then, specifyForge2dGameWorld
as theBodyComponent
game world type. This association gives your ball class access to the public properties of your game world. - When you create your ball, you specify its initial position and size.
- You tell Forge2D how to create your body in
createBody
. Forge2D callscreateBody
when you add a body to the game world. - Define the base properties of the ball’s body. The ball moves freely around the world, so its type is dynamic. The position will be passed into the constructor when adding the ball to the world; this allows you to set the beginning position of the ball.
- Use the body definition to create a rigid body in your game world.
world
is an inherited property fromBodyComponent
to yourForge2dGameWorld
instance. - If the
Body
is the soul of the rigid body,Fixture
s are its skin and bones. To define a fixture, you begin by defining a shape. In this case, your ball will have a circle shape in this 2D world. - Using the shape, you create a fixture definition.
- Use the ball’s
createFixture
method to create and add the fixture to the ball’s body.
Next, create the ball and add it to your Forge2D world by opening the file forge2d_game_world.dart and creating a private method named _initializeGame
. Now, call the routine from onLoad
like so:
import 'package:flame_forge2d/flame_forge2d.dart';
import 'components/ball.dart';
class Forge2dGameWorld extends Forge2DGame {
@override
Future<void> onLoad() async {
await _initializeGame();
}
Future<void> _initializeGame() async {
final ball = Ball(
radius: 1.0,
position: size / 2,
);
await add(ball);
}
}
Give the ball a radius
of 1.0 and a starting position
in the center of the game area. size
provides you with the size of the visible game area in the Forge2dGameWorld
. A discussion of Forge2D units, coordinates, viewports and camera are coming. So, use these values for now with the understanding that you’ll get an explanation shortly.
Build and run your project, and you’ll see a small white circle representing your ball falling off the bottom of the screen.
What’s going on? Where did the ball go? The ball is still there in your Forge2D world. It’s just forever falling into the vast, dark emptiness beyond the bottom of the screen, much like Voyager 1 and 2 speeding through space.
A ball falling off the screen isn’t much fun. So next, you’ll learn how to build walls to constrain the ball to the game area.
Creating a Game Arena
A Forge2D game world is more like a vast, empty space than a world. You create the bodies and other components of your world to fill the space.
The user plays Breakout within an enclosed area, like an arena. You’ll now create an arena to constrain the ball to a fixed region of your world. Create an arena.dart file in the components folder, and add the following lines of code to this file:
import 'package:flame_forge2d/flame_forge2d.dart';
import '../forge2d_game_world.dart';
// 1
class Arena extends BodyComponent<Forge2dGameWorld> {
Vector2? size;
// 2
Arena({this.size}) {
assert(size == null || size!.x >= 1.0 && size!.y >= 1.0);
}
late Vector2 arenaSize;
// 3
@override
Future<void> onLoad() {
arenaSize = size ?? gameRef.size;
return super.onLoad();
}
// 4
@override
Body createBody() {
final bodyDef = BodyDef()
..position = Vector2(0, 0)
..type = BodyType.static;
final arenaBody = world.createBody(bodyDef);
// 5
final vertices = <Vector2>[
arenaSize,
Vector2(0, arenaSize.y),
Vector2(0, 0),
Vector2(arenaSize.x, 0),
];
// 6
final chain = ChainShape()..createLoop(vertices);
// 7
for (var index = 0; index < chain.childCount; index++) {
arenaBody.createFixture(FixtureDef(chain.childEdge(index)));
}
return arenaBody;
}
}
The arena has many of the same elements you learned when creating the ball’s body. It can be helpful to go over what’s the same and what’s new step-by-step:
- The arena is another body component in your game world. It acts like a fence enclosing the objects in your game.
- The
Arena
constructor has an optionalsize
parameter for defining the extent of the rectangular arena. Leave this blank; theonLoad
method will set the size to fill the available widget space. -
onLoad
is aBodyComponent
state method.onLoad
is called beforecreateBody
to allow for any initialization you might need to perform. Here, you’re getting the size of the visible area in Forge2D world coordinates. You’ll learn more about world coordinates in the next section. - You’ve seen this method before. Here’s where you build your arena body. Setting the
position
to the world origin aligns the arena with the upper left-hand corner of theGameWidget
. Since the arena walls won’t move, the arena’s body type is static. - With the arena body created, you now need to define its fixtures. The arena’s fixtures will be the walls that enclose the area. Forge2D has a
ChainShape
, a free-form sequence of line segments perfect for the arena enclosure. So first, you create a list of the locations of the arena’s four corners. - Then, create a
ChainShape
from the vertex list. ThecreateLoop
method automatically closes the loop for you. - Now, create fixtures for each edge of the chain.
ChainShape
provides an excellent method that returns anEdgeShape
for each segment in the chain to use to create the fixtures of the arena. These are the walls of your arena.
Now, you need to instantiate the arena and add it to your Forge2D world. Open the file forge2d_game_world.dart, add an import for arena.dart and create an instance of Arena
in _initializeGame
above where you instantiate the Ball
:
import 'components/arena.dart';
Future<void> _initializeGame() async {
final arena = Arena();
await add(arena);
Build and run your project. The white circle now falls and stops at the bottom edge of the GameWidget
area. Congratulations! You’ve corralled the ball and are well along the way to creating your Breakout game.
Understanding Forge2D Units and Coordinates
You’ve created a GameWidget
in Flutter, added a Forge2dGameWorld
and created two rigid bodies: a dynamic ball and a static arena. In doing so, you used values for the ball’s radius and position and the location of the arena’s walls. So, what’s the context for these units and coordinates?
The Forge2D world is more or less infinite, or at least as limitless as a simulated 2D world can be. This vastness is because you specify most units in Forge2D using the double
data type, which can represent a wide range of values. But what do these unit values mean?
Units
Erin Catto, the creator of Box2D — the direct ancestor of Forge2D — wrote Box2D to be tuned for a world of MKS units (meters/kilograms/seconds). This unit tuning is inherent to Forge2D as well. Catto wrote in the Box2D documentation:
“… it is tempting to use pixels as your units. Unfortunately, this will lead to a poor simulation and possibly weird behavior. An object of length 200 pixels would be seen by Box2D as the size of a 45 story building.”
A 45-story building? How is that? Well, a length of 200.0 in Forge2D is essentially 200 meters. So, a story on a building is roughly 4.4 meters or 14 feet; 200 divided by 4.4 is 45.4545, thus a 45-story building.
Catto recommends keeping the size of moving objects between 0.1 and 10 meters, roughly from the size of a soup can up to a school bus. He also recommends keeping the world size to less than 2 kilometers.
The remaining units used in Forge2D are angles, measured in radians and not degrees; mass, measured in kilograms and time, measured in seconds.
Coordinate Systems
The Forge2D game world uses a typical, two-dimensional Cartesian coordinate system. You learned that while length units aren’t explicitly defined, you must think of them in terms of meters. Lengths, forces, distances and positions are all defined by two-dimensional vectors of meter values. For example, a vector of Vector2(2,3)
extends two meters in the x-direction and three meters in the y-direction in Forge2D.
Flutter also uses a two-dimensional Cartesian coordinate system, but its units are device-independent pixels with an inverted y-axis.
So, what does this mean for your Breakout game? First, you must remember to use device-independent pixels when giving size, position and offset values to Flutter widgets and meters when giving similar values to Forge2D components. Second, when transitioning between the two coordinate spaces, you need to convert between screen coordinates, which are Flutter’s device-independent pixels, and world coordinates, which are Forge2D’s metric units.
Flame and Forge2D provide tools to help you with these translations. For example, the flame_forge2d
package inverts Forge2D world y-axis values to align with screen coordinates. In addition, there are several methods for converting positions between the screen and the Forge2D world.
The Flame Camera
When you peer into the Forge2D world through the GameWidget
, you’re looking through the lens of a Camera. The Camera
translates the Forge2D coordinate system to your screen size. Forge2DGame
provides your game with a reasonable default camera. There’s no translation, so the camera’s position is set to the origin of the Forge2D world. The camera zoom is 10.0. Remember Catto’s recommendation not to use pixel units for your Forge2D world? A camera zoom effectively makes 10 device-independent pixels equal to one meter. For example, a GameWidget
with a size of 330 by 760 pixels, a typical screen size, means the visible Forge2D world is 33.0 by 76.0 meters.
Breakout Game Metrics
Looking at the settings used in your Breakout game, you have a 2.0-meter ball (1.0 radius) moving in an arena of approximately 33.0 by 76.0 meters on most mobile devices. Or, in English units, a 6.5-foot ball in a 36.0-by-81.1-yard arena. A Chrome browser will have a more variable arena size. That’s a big ball in a large arena.
Why does this matter? Speed. The ball in a Breakout game moves fast, traveling the length of the game area in 1 or 2 seconds or less. That means the ball travels at 160 km/hr (100 m/hr) or more. Whew!
Fine-tuning the parameters of bodies in a Forge2D world is a bit of intuition mixed with some experimentation. In the remaining sections, you’ll use parameters that produce a playable game, but feel free to experiment and try your values.
Adjusting the Camera, Arena and Ball Body Properties
Applying your newfound knowledge to your Breakout game, you’ll first adjust the size of the Forge2D world your Breakout game occupies. Add the following constructor at the top of Forge2dGameWorld
in forge2d_game_world.dart:
Forge2dGameWorld() : super(gravity: Vector2.zero(), zoom: 20);
The default gravitational force is Vector2(0.0, 10.0)
; this is why the ball falls to the bottom of the screen. Remember, flame_forge2d
inverts the y-axis to align with the screen. Breakout doesn’t need gravity, so setting gravity
to Vector2.zero()
turns it off, like floating in space.
Setting the zoom
parameter halves the size of the world from its previous setting by equating 20 device-independent screen pixels to one meter. As a result, the screen area of 330 by 760 pixels is now an 18.0-by-40.5-yard arena.
Run your project now, and you’ll see the ball has doubled in size and remains stationary in the center of the GameWidget
.
That’s not very interesting. So next, you’ll adjust the properties of the arena and ball. Open arena.dart, and change the FixtureDef
to add density, friction and restitution properties to the arena walls.
for (var index = 0; index < chain.childCount; index++) {
arenaBody.createFixture(
FixtureDef(chain.childEdge(index))
..density = 2000.0
..friction = 0.0
..restitution = 0.4,
);
}
A density of 2,000 kg/m^2 is a substantial concrete wall. In addition, the surface is frictionless, so there’s no loss of moment from the ball contacting the wall. Finally, give the wall some elastic recoil with a restitution value of 40%.
Now, adjust the properties of the ball. Open ball.dart, and change FixtureDef
to add restitution and density to the ball:
final fixtureDef = FixtureDef(shape)
..restitution = 1.0
..density = 1.0;
Restitution of 100% is a very bouncy ball. A density of 1 kg/m^2 is acceptable for the ball.
To complete your adjustments, open forge2d_game_world.dart. Since the world has no gravity now, you’ll need to apply a force to the ball to set it in motion. Make the following changes to your Forge2D game:
class Forge2dGameWorld extends Forge2DGame {
Forge2dGameWorld() : super(gravity: Vector2.zero(), zoom: 20);
// 1
late final Ball _ball;
@override
Future<void> onLoad() async {
await _initializeGame();
// 2
_ball.body.applyLinearImpulse(Vector2(-10, -10));
}
Future<void> _initializeGame() async {
final arena = Arena();
await add(arena);
// 3
_ball = Ball(
radius: 0.5,
position: size / 2,
);
await add(_ball);
}
}
In this code, you:
- Create a private variable reference for the ball.
- Apply a force to the ball after Forge2D has completed creating the ball and adding it to the world.
- Use the class level
_ball
variable and adjust the size of the ball to have a radius of 0.5.
Build and run your project. The ball now ricochets off the arena walls in the GameWidget
area. Congratulations! You’ve taken another important step toward creating your Breakout game.
Where to Go From Here?
You can download the completed project files by clicking the Download Materials button at the top or bottom of the tutorial.
This concludes Part 1 of this tutorial. Here, you’ve learned how to:
- Create a Flame
GameWidget
with aForge2DGame
child in a Flutter app. - Create and add
BodyComponent
s describing rigid bodies in Forge2D’s simulated 2D world. - Use the Flame game loop to initialize a game.
- Define the physical properties of rigid bodies in Forge2D.
Remember that if you want to learn more about the Flame Engine and Forge2d you can always check out their documentation here and here respectively. Also, as mentioned before, you can read our Building Games with Flutter tutorial here for more game building fun.
In Parts 2 and 3 of the Create A Breakout Game With Flame and Forge2D tutorial, you’ll learn how to create the remaining components for your Breakout game, add gameplay logic and create a visual skin for your game.
We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!
[ad_2]
Source link