Table of contents
Building apps with Flutter is really cool. You know what’s cooler?
Building games with flutter. This article will show you how to build a flutter game from start to finish with a flutter flame tutorial.
Let’s create a flutter game! We can build the classic pong game in Flutter using the 🔥Flame game engine.
Flame is a 2D game engine built for Flutter. It’s built on top of the framework and simplifies game development. Flame provides us with everything we’ll need to build a flutter game.
Flutter Game | Pong Game with Flame
Some of the concepts we’ll learn are: flutter game development
Collision detection in Flame
Building a simple AI opponent
Using Flame audio flutter
Note: Knowledge of the basics of Flutter and Flame are required for this tutorial. Check out the Flutter Flame docs if you’re new to the engine.
Grab a coffee; let’s get started! Flutter game on! 🎮
Flutter Game: Getting Started with game development in flutter
Let’s create a new flutter project and enter the folder with the following commands:
flutter create pong_game
cd pong_game
Next, add the required Flame dependencies:
flutter pub add flame
For the game, our file structure will look like this:
-lib/
··|---main.dart
··|---pong_game.dart
··|---player_paddle.dart
··|---ball.dart
··|---ai_paddle.dart
··|---scoretext.dart
We’ll update our main.dart with the following code:
void main() {
final game = PongGame();
runApp(GameWidget(game: game));
}
Now, in the pong_game.dart file, we’ll add the the following:
class PongGame extends FlameGame
with HasCollisionDetection, HasKeyboardHandlerComponents {
PongGame();
@override
Future<void> onLoad() async {}
@override
@mustCallSuper
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
super.onKeyEvent(event, keysPressed);
return KeyEventResult.handled;
}
}
Here, we have the PongGame
declared. Notice that it has two mixings: HasCollisionDetection
and HasKeyboardHandlerComponents
. This will let Flame know that our game is going to use these two things and allow us to work with collision detection and take keyboard inputs at the component level.
We’re also overriding the onKeyEvent
here and returning KeyEventResult.handled
. This is because if you’re on macOS, then you’ll notice key press sounds as you’re receiving keyboard inputs in the game. Returning KeyEventResult.handled
will disable those sounds.
Build & run:
Flutter Game: Flutter Flame Example
Flutter Flame Collision Detection
Before moving on to building our game, let’s take a look at how collision detection works in Flame. This will be important for us as we’ll need to set up HitBoxes for our game bodies, know when these bodies collide with each other and react accordingly.
HitBoxes
In many game systems, collision detection works by having a HitBox around the game object. HitBoxes react to collisions and can send callbacks with the collision information.
Flame supports adding different HitBoxes to our components like PolygonHitBox
, RectangleHitBox
, CircleHitBox
or ScreenHitBox
, which is usually used for declaring the world boundaries/screen edges that components may collide with.
Note: We can use multiple HitBoxes on a component to provide more accurate collision detection for it. For example, a game character can have separate HitBoxes around its arms, its legs, and so on.
Enable Collision Detection
For this, we first need to add the HasCollisionDetection
mixing to our Flame game.
For the components, we want to get notified when they collide with other bodies that are capable of collision. For this, we’ll add the CollisionCallbacks
mixing to those components.
class MyComponent extends PositionComponent with CollisionCallbacks {
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
// TODO: implement onCollision
super.onCollision(intersectionPoints, other);
}
@override
void onCollisionStart(Set<Vector2> intersectionPoints, PositionComponent other) {
// TODO: implement onCollisionStart
super.onCollisionStart(intersectionPoints, other);
}
@override
void onCollisionEnd(PositionComponent other) {
// TODO: implement onCollisionEnd
super.onCollisionEnd(other);
}
}
Adding this mixing allows us to be notified when a body collides with other bodies through callbacks such as onCollision
, onCollisionStart
and onCollisionEnd
. These callbacks also provide the intersection points and the reference to the other body the component is colliding with.
Note: The Collision Detection API only lets us know when two bodies collide. What happens upon collision is up to us!
Now, let’s move on to the different components of our game.
Game Components
Our Pong game mainly consists of the following components:
Game Boundaries
Player paddle
Ball
AI opponent paddle
Scoring system
Game Boundaries
Our ball is going to collide with the boundaries of our game/screen. We need to know when this happens so that we can either bounce it off of the top or bottom of the screen or update the players’ score if it’s colliding with the left or right of the screen.
For this, we’ll declare game boundaries by adding the ScreenHitBox
component.
Replace the onload
method within PongGame
with the following:
@override
Future<void> onLoad() async {
addAll([
ScreenHitbox()
]);
}
ScreenHitBox
will represent the edges of our game screen. If any other components collide with the edges, we’ll be notified of the collision.
Player Paddle
Now, we’ll add the player paddle to the flutter game tutorial.
Create a new file called player_paddle.dart
and add the following to it:
// TODO: add key event enum
class PlayerPaddle extends PositionComponent
with HasGameRef<FlameGame>, CollisionCallbacks {
late final RectangleHitbox paddleHitBox;
late final RectangleComponent paddle;
// TODO: add variable key event and speed variables
@override
Future<void>? onLoad() {
// TODO: implement onLoad
final worldRect = gameRef.size.toRect();
size = Vector2(10, 100);
position.x = worldRect.width * 0.9 - 10;
position.y = worldRect.height / 2 - size.y / 2;
paddle = RectangleComponent(
size: size,
paint: Paint()..color = Colors.blue,
);
paddleHitBox = RectangleHitbox(
size: size,
);
addAll([
paddle,
paddleHitBox,
]);
// TODO: add keyboard listener component
return super.onLoad();
}
//TODO: add update code for moving paddle
}
Our PlayerPaddle
is a PositionComponent
with the HasGameRef
and CollisionCallbacks
mixing. The HasGameRef
mixing will allow us to get the game reference and check for any values in our game world. CollisionCallbacks
mixing, as we discussed, will add support for setting collision callbacks.
In the onLoad
method, we’re setting the size for our paddle component and positioning it at the center-right of the screen. We also added a RectangleHitBox
of the same size as our paddle so that it can detect collisions.
Within the onload
method of the PongGame
component, add the PlayerPaddle
:
@override
Future<void> onLoad() async {
addAll(
[
...
.....
PlayerPaddle(),
],
);
}
Build & run: flutter flame example
Player keyboard controls
Flame offers two different ways to take keyboard inputs; one at the game level and the other at the component level.
Let’s take a look at receiving keyboard inputs at the component level. You can learn more about other ways of taking keyboard input here.
We’ll make sure our PongGame
component has the HasKeyboardHandlerComponents
mixing. Within our PlayerPaddle
component, we’ll use the KeyboardListenerComponent
, through which we can set callbacks for different key events.
Add the following component within your onload
method:
add(
KeyboardListenerComponent(
keyDown: {
LogicalKeyboardKey.arrowDown: (keysPressed) {
return true;
},
LogicalKeyboardKey.arrowUp: (keysPressed) {
return true;
},
},
keyUp: {
LogicalKeyboardKey.arrowDown: (keysPressed) {
return true;
},
LogicalKeyboardKey.arrowUp: (keysPressed) {
return true;
},
},
),
);
This adds the KeyboardListenerComponent
. We’ll be registering callbacks for arrowDown
and arrowUp
events when the respective keys are either pressed or released.
Moving Player Paddle
Now that we’re receiving keyboard events, let’s see how we can move our paddle.
Let’s try updating our paddle position along the y-axis by 50 when the down arrow is pressed and by -50 when the up arrow is pressed. Update keyDown
within the KeyboardListenerComponent
with the following (you may need to hot restart your game to reflect the new changes):
keyDown: {
LogicalKeyboardKey.arrowDown: (keysPressed) {
position.y += 50;
return true;
},
LogicalKeyboardKey.arrowUp: (keysPressed) {
position.y -= 50;
return true;
},
},
Build & run: game in flutter
Flutter Game
You’ll see that the paddle moves, but its movement is janky. It’s not smooth. 🤷
This is because the position is not updated consistently with the passage of time. To achieve smooth movement, we’ll need to update its position from within the update
method.
Currently, in Flame, there’s no possible way to know which keys are pressed within the update
method. For this, we’ll first set up a variable that’ll tell us which key was pressed so we can update the position accordingly.
Replace the // TODO: add key event enum
within player_paddle.dart
with the following code:
enum KeyEventEnum {
up,
down,
none,
}
Declare the following variables within the paddle component:
KeyEventEnum keyPressed = KeyEventEnum.none;
static const double speed = 400;
keyPressed
: Lets us know which key is pressed. When none of the keys are pressed, we’ll update this variable toKeyboardEventEnum.none
, so we can know to stop updating the position.speed
: Paddle moving speed.
Replace the previously added KeyboardListenerComponent
with the following:
add(
KeyboardListenerComponent(
keyDown: {
LogicalKeyboardKey.arrowDown: (keysPressed) {
keyPressed = KeyEventEnum.down;
return true;
},
LogicalKeyboardKey.arrowUp: (keysPressed) {
keyPressed = KeyEventEnum.up;
return true;
},
},
keyUp: {
LogicalKeyboardKey.arrowDown: (keysPressed) {
keyPressed = KeyEventEnum.none;
return true;
},
LogicalKeyboardKey.arrowUp: (keysPressed) {
keyPressed = KeyEventEnum.none;
return true;
},
},
),
);
Here, we’re doing two things:
Setting the
KeyboardEventEnum
to up/down based on the key pressed.Resetting it to
KeyEventEnum.none
when the key is released.
Add the following code, which overrides the update method for the component:
@override
void update(double dt) {
// TODO: implement update
super.update(dt);
if (keyPressed == KeyEventEnum.down) {
final updatedPosition = position.y + speed * dt;
if (updatedPosition < gameRef.size.y - paddle.height) {
position.y = updatedPosition;
}
}
if (keyPressed == KeyEventEnum.up) {
final updatedPosition = position.y - speed * dt;
if (updatedPosition > 0) {
position.y = updatedPosition;
}
}
}
Here, we update the paddle position based on the key pressed. This time, instead of passing a fixed displacement, we’re updating the position by the speed\dt(=distance)* value.
We also check if our paddle is going out of the bounds of the game window. If it is, then we stop updating the position.
We can test our updates by holding down the up or down arrow keys and seeing the paddle move smoothly.
Build & run:
Flutter Game
Flutter Game: Adding the Ball
Create a new file called ball.dart and add the following code to it:
import 'dart:math' as math;
class Ball extends CircleComponent
with HasGameRef<PongGame>, CollisionCallbacks {
Ball() {
paint = Paint()..color = Colors.white;
radius = 10;
}
// 1.
late Vector2 velocity;
// 2.
static const double speed = 500;
// 3.
static const degree = math.pi / 180;
// 6.
@override
Future<void>? onLoad() {
_resetBall;
final hitBox = CircleHitbox(
radius: radius,
);
addAll([
hitBox,
]);
return super.onLoad();
}
@override
void update(double dt) {
super.update(dt);
position += velocity * dt;
}
// 4.
void get _resetBall {
position = gameRef.size / 2;
final spawnAngle = getSpawnAngle;
final vx = math.cos(spawnAngle * degree) * speed;
final vy = math.sin(spawnAngle * degree) * speed;
velocity = Vector2(
vx,
vy,
);
}
// 5.
double get getSpawnAngle {
final sideToThrow = math.Random().nextBool();
final random = math.Random().nextDouble();
final spawnAngle = sideToThrow
? lerpDouble(-35, 35, random)!
: lerpDouble(145, 215, random)!;
return spawnAngle;
}
}
Our ball is a CircleComponent
, which is a PositionedComponent
but circular with HasGameRef
and CollisionCallbacks
mixing. We also defined the color and radius of the ball within its constructor.
Along with defining the HitBox for our ball in the onload
method, we have some other things here:
velocity
: A 2D vector representing the ball's velocity.speed
: A constant value that will calculate the ball's velocity.degree
: The degree to radian constant._resetBall
: Spawns (positions) the ball at the center of the screen and launches it in a random direction with some initial velocity.getSpawnAngle
: Calculates the angle at which the ball will be thrown upon spawning.
Finally, within the update
method, we update the ball's position with respect to its velocity
and the time passed, i.e., dt
.
Let’s add the ball component to our PongGame
component:
@override
Future<void> onLoad() async {
addAll(
[
...
.....
Ball(),
],
);
}
Build & run:
Flutter Game
Collision Detection with the Ball
Now that we have our Ball
spawning in the center of the screen and moving, let's get to the interesting part of the game: making the ball bounce when it collides with a PlayerPaddle
or the top and bottom edges of the game.
Add the following code, which overrides the onCollisionStart
method within the Ball
component.
@override
@mustCallSuper
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
final collisionPoint = intersectionPoints.first;
// TODO: add edges collision update
// TODO: add player paddle collision update
// TODO: add ai paddle collision update
}
This callback provides us with the intersection/collision points for our component and the ref to the component we are colliding with. These will be useful in the next section, where we deal with collision logic for different bodies.
Edge Collision Update
We’ll first update the ball velocity to bounce off of the top and bottom edges of the screen. Replace the // TODO: add edge collision update
with the following code:
if (other is ScreenHitbox) {
// Left Side Collision
if (collisionPoint.x == 0) {
// TODO: update player score
}
// Right Side Collision
if (collisionPoint.x == gameRef.size.x) {
// TODO: update ai score
}
// Top Side Collision
if (collisionPoint.y == 0) {
velocity.x = velocity.x;
velocity.y = -velocity.y;
// TODO: play the collision sound
}
// Bottom Side Collision
if (collisionPoint.y == gameRef.size.y) {
velocity.x = velocity.x;
velocity.y = -velocity.y;
// TODO: play the collision sound
}
}
Here, we’re first checking if the body that our ball collided with is ScreenHitBox
or not. If it is, we check for the edge with which our ball collided.
We don’t want the ball to bounce off of the left and right edges. We’ll later add some code there to update the players’ scores.
If it’s the top or bottom edge, we reverse the ball’s velocity
in the y direction. Test it by changing the ball’s spawnAngle
to 90 such that it’ll be thrown towards the top or bottom edges.
Build & run:
Flutter Game
Paddle Collision Update
Replace the // TODO: paddle collision update
with the following:
if (other is PlayerPaddle) {
final paddleRect = other.paddle.toAbsoluteRect();
updateBallTrajectory(collisionPoint, paddleRect);
// TODO: play the collision sound
}
If the collided object is the PlayerPaddle
, we first calculate the paddleRect
, which is the bounding rectangle of the component in the global coordinate space.
Within the Ball
component, add the following method:
void updateBallTrajectory(Vector2 collisionPoint, Rect paddleRect) {
final isLeftHit = collisionPoint.x == paddleRect.left;
final isRightHit = collisionPoint.x == paddleRect.right;
final isTopHit = collisionPoint.y == paddleRect.bottom;
final isBottomHit = collisionPoint.y == paddleRect.top;
final isLeftOrRight = isLeftHit || isRightHit;
final isTopOrBottom = isTopHit || isBottomHit;
if (isLeftOrRight) {
velocity.x = -velocity.x;
velocity.y = velocity.y;
}
if (isTopOrBottom) {
velocity.x = velocity.x;
velocity.y = -velocity.y;
}
}
This method will reverse the ball’s velocity
along the x- or y-axis, depending on where it touches the paddle, which is known by checking the collisionPoint
with the paddleRect
position. If the collision is on the left or right side, we reverse the velocity
along the x-axis. If the collision is on the top or bottom, we reverse the velocity along the y-axis.
Build & run:
Flutter Game
Flutter Game: AI Paddle
Now that we’ve got the ball bouncing off the edges and the paddle, let’s add the AI opponent 🤖 you can play against.
It’ll be very similar to how we did the PlayerPaddle
; the only part that’s going to be different is how it moves.
Add the following code to a new file called ai_paddle.dart:
class AIPaddle extends PositionComponent
with HasGameRef<FlameGame>, CollisionCallbacks {
late final RectangleHitbox paddleHitBox;
late final RectangleComponent paddle;
@override
Future<void>? onLoad() {
// TODO: implement onLoad
final worldRect = gameRef.size.toRect();
size = Vector2(10, 100);
position.x = worldRect.width * 0.1;
position.y = worldRect.height / 2 - size.y / 2;
paddle = RectangleComponent(
size: size,
paint: Paint()..color = Colors.red,
);
paddleHitBox = RectangleHitbox(
size: size,
);
addAll([
paddle,
paddleHitBox,
]);
return super.onLoad();
}
}
Construction of our AI paddle is very similar to the PlayerPaddle
, except we position it at the center on the left side.
Don’t forget to add the AIPaddle
component to our PongGame
component:
@override
Future<void> onLoad() async {
addAll(
[
...
.....
AIPaddle(),
],
);
}
Build & run:
Flutter Game
AI Paddle Movement Logic
There are many different ways to build this AI opponent, control its behavior, detect how it should move, set how fast it should move and decide how challenging it should be to play against.
For our game, we won’t be building an AI that will be literally impossible to beat, just a simple AI that we can play against peacefully. ✌️
Our AI Paddle will follow two rules depending on the ball's position:
If the
AIPaddle
is below theBall
, it should move up towards the ball.If the
AIPaddle
is above theBall
, it should move down towards the ball.
Following these rules, override the update
method for AIPaddle
with the following:
@override
void update(double dt) {
// TODO: implement update
super.update(dt);
final ball = gameRef.children.singleWhere((child) => child is Ball) as Ball;
if (ball.y > position.y) {
position.y += (400 * dt);
}
if (ball.y < position.y) {
position.y -= (400 * dt);
}
}
Here, we first get the reference to the ball from our game world. Depending on the earlier rules we defined, we move the AIPaddle
up or down.
In some cases, the AIPaddle
will follow the ball even if it goes outside the game boundaries; to prevent this, replace the code after we query/get the ball with the following:
final ballPositionWrtPaddleHeight = ball.y + (size.y);
final isOutOfBounds =
ballPositionWrtPaddleHeight > gameRef.size.y || ball.y < 0;
if (!isOutOfBounds) {
if (ball.y > position.y) {
position.y += (400 * dt);
}
if (ball.y < position.y) {
position.y -= (400 * dt);
}
}
Here, we check if the updated position will be within the boundaries of our game world. If it isn’t, we don’t update the position of the paddle.
Build & run:
Flutter Game
AI Collision Update
Within the Ball
component’s update
method, replace // TODO: add AI paddle collision update
with the following:
if (other is AIPaddle) {
final paddleRect = other.paddle.toAbsoluteRect();
updateBallTrajectory(collisionPoint, paddleRect);
// TODO: play the collision sound
}
Now, our ball will also collide with the AIPaddle
and bounce off of it after a collision.
Build & run:
Flutter Game
Add the Scoring System
Now onto the final part of the game— adding the scoring system. Create a new file called score_text.dart and add the following to it:
class ScoreText extends TextComponent with HasGameRef<PongGame> {
late int score;
ScoreText.aiScore({
this.score = 0,
}) : _textPaint = TextPaint(textDirection: TextDirection.ltr),
super(
anchor: Anchor.center,
);
ScoreText.playerScore({
this.score = 0,
}) : _textPaint = TextPaint(textDirection: TextDirection.rtl),
super(
anchor: Anchor.center,
);
late final TextPaint _textPaint;
@override
Future<void>? onLoad() {
score = 0;
final textOffset =
(_textPaint.textDirection == TextDirection.ltr ? -1 : 1) * 50;
position.setValues(gameRef.size.x / 2 + textOffset, gameRef.size.y * 0.1);
text = score.toString();
return super.onLoad();
}
@override
void render(Canvas canvas) {
_textPaint.render(canvas, '$score', Vector2.zero());
}
}
This ScoreText
will hold and display the score for each player. It has two factory constructors; one for aiScore
and one for player
. Within its onLoad
method, we position our scores at the top center and offset them a little in the left or right direction based on whether it’s the player’s or the AI’s score.
We’ve also overridden the render
method to show the latest score as it’s updated.
Now, within our PongGame
component, add the following aiScore
and playerScore
variables which will hold the ScoreText
component:
late final ScoreText aiPlayer;
late final ScoreText player;
Update the addAll
method by adding these two components:
aiPlayer = ScoreText.aiScore(),
player = ScoreText.playerScore(),
Now that we have the score components in place, the next thing we want to do is update the scores whenever the player or the AI scores.
Update the Score
Within the onCollisionStart
method of our Ball
component, replace the code from // Left Side Collision
to // Right Side Collision
with the following:
// Left Side Collision
if (collisionPoint.x == 0) {
final player = gameRef.player;
updatePlayerScore(player);
}
// Right Side Collision
if (collisionPoint.x == gameRef.size.x) {
final player = gameRef.aiPlayer;
updatePlayerScore(player);
}
Add the following updatePlayerScore
method in the Ball
component:
import 'dart:async' as dartAsync;
void updatePlayerScore(ScoreText player) {
player.score += 1;
dartAsync.Timer(const Duration(seconds: 1), () {
_resetBall;
});
}
This method takes in the ScoreText
object and increments its score
by 1. After that, we set up a timer for 1 second to respawn the ball in the center by calling _resetBall
.
Now as you or the AI opponent misses the ball, the opposite player will get the point and their score will be updated.
Build & run:
Save Code Snippets Instantly from Articles
10x your developer productivity and store code snippets from any blog - and so much more.
Flutter Game: Adding Collision Audio
A game without audio is definitely not something you would play. So, let’s add a collision sound whenever the ball collides with other game bodies.
Run the following command to add the flame_audio dependency:
flutter pub add flame_audio
Once that’s done, download the audio file for the collision sound here. Add the audio files to the assets/audio
folder. Make sure to add the audio folder to the assets section in the pubspec as shown:
Flutter Flame Audio
Let’s add the following method in our Ball
component:
void get _playCollisionAudio {
FlameAudio.play("ball_hit.wav");
}
We’ll need to play the collision sound after every collision. Within the onCollisionStart
method of the Ball
component, replace the //TODO: play the collision sound
with:
_playCollisionAudio
Final demo:
Flutter Game
Bonus
In the final demo, our Ball speeds up a little when it collides with either the player’s paddle or the AI paddle. For this, we’re simply increasing the ball's velocity in the y-direction by giving it some additional nudgeSpeed
. I suggest making the nudge speed 300/200, but you can make it whatever you prefer.
Flutter Game
Flutter Game: Summary
Congrats! 🥳 We just built a Pong game with Flame!🔥
While building this game, we learned about:
CollisionDetection API in Flame.
Building a simple AI opponent.
Adding a scoring system to the game.
Adding audio to your game.
You can download the source code here.
Next
Flame has been growing steadily in the Flutter community and many exciting things are coming up in the recent updates. Check out the Awesome Flame repository for some amazing examples built with Flame.😋
Flame will continue to grow and allow us to build cool games with Flutter. We at Pieces are really excited about it. Stay tuned for our upcoming articles where we’ll explore Flame to build amazing games! 🎮