Building Games in Flutter with Flame: Getting Started
Learn how to build a beautiful game in Flutter with Flame. In this tutorial, you’ll build a virtual world with a movable and animated character. By Vincenzo Guzzi.
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
Building Games in Flutter with Flame: Getting Started
30 mins
- Getting Started
- The Flame Game Engine
- Setting up Your Flame Game Loop
- Creating Your Player
- Adding Movement to Your Player
- Executing on Player Movement
- Animating Your Player
- What Is a Sprite Sheet?
- Adding Sprite Sheet Animations to Your Player
- Adding a World
- Adding World Collision to Your Game
- Creating Tile Maps
- Creating World Collision in RayWorld
- Bonus Section: Keyboard Input
- Where to Go From Here?
Adding a World
Create a file called world.dart
in your components folder. In world.dart
, create a SpriteComponent called World and load rayworld_background.png as the world sprite:
import 'package:flame/components.dart';
class World extends SpriteComponent with HasGameRef {
@override
Future<void>? onLoad() async {
sprite = await gameRef.loadSprite('rayworld_background.png');
size = sprite!.originalSize;
return super.onLoad();
}
}
Head back to RayWorldGame
and add World
as a variable under Player
:
final World _world = World();
Make sure to add the World
import, too:
import 'components/world.dart';
Now, add _world
to your game at the beginning of onLoad
:
await add(_world);
You must load the world completely before loading your player. If you add the world afterward, it will render on top of your Player
sprite, obscuring it.
Build and run, and you’ll see a beautiful pixel landscape for your player to run around in:
For your player to traverse the world properly, you’ll want the game viewport to follow the main character whenever they move. Traditionally, when programming video games, this requires a plethora of complicated algorithms to accomplish. But with Flame, it’s easy!
At the bottom of your game onLoad
method, set the player’s initial position the center of the world and tell the game camera to follow _player
:
_player.position = _world.size / 2;
camera.followComponent(_player,
worldBounds: Rect.fromLTRB(0, 0, _world.size.x, _world.size.y));
Add the import for using a Rect
variable at the top of the file:
import 'dart:ui';
Build and run, and you’ll see your world sprite pan as your player moves. As you’ve set the worldBounds
variable, the camera will even stop panning as you reach the edge of the world sprite. Run to the edge of the map and see for yourself.
Congratulations!
You should be proud of yourself for getting this far. You’ve covered some of the core components needed in any game dev’s repertoire.
However, there’s one final skill you must learn to be able to make a full game: Collision detection.
Adding World Collision to Your Game
Creating Tile Maps
2-D game developers commonly employ tile maps. The technique involves creating artwork for your game as a collection of uniform tiles you can piece together however needed like a jigsaw, then creating a map you can use to tell your game engine which tiles go where.
You can make tile maps as basic or as advanced as you like. In a past project, a game called Pixel Man used a text file as a tile map that looked something like this:
xxxxxxxxxxx
xbooooooox
xoooobooox
xoooooooox
xoooooboox
xxxxxxxxxxx
The game engine would read these files and replace x’s with walls and b’s with collectable objects, using the tile map for both logic and artwork purposes.
These days, software makes the process of creating a tile map a lot more intuitive. RayWorld uses software called Tiled. Tiled is free software that lets you create your levels with a tile set and add additional collision layers in a graphical editor. It then generates a tile map written in JSON that can be easily read in your game engine.
A tile map called rayworld_collision_map.json already exists. You’ll use this JSON file to add collision objects into your game in the next section. It looks like this in the Tiled editor:
The pink boxes are the collision rectangles. You’ll use this data to create collision objects in Flame.
Creating World Collision in RayWorld
Add a file in your components folder called world_collidable.dart and create a class called WorldCollidable
:
import 'package:flame/components.dart';
import 'package:flame/geometry.dart';
class WorldCollidable extends PositionComponent
with HasGameRef, Hitbox, Collidable {
WorldCollidable() {
addHitbox(HitboxRectangle());
}
}
This component adds Hitbox
and Collidable
mixins that tells Flame to make this component collidable based on the HitboxRectangle
that you’ve added. This hitbox will match the size of the position component with a collidable box. You’ll add many of these components to your game — one for each rectangle loaded by the collision map.
Create a method in RayWorldGame
called addWorldCollision
:
void addWorldCollision() async =>
(await MapLoader.readRayWorldCollisionMap()).forEach((rect) {
add(WorldCollidable()
..position = Vector2(rect.left, rect.top)
..width = rect.width
..height = rect.height);
});
Here, you use a helper function, MapLoader
, to read rayworld_collision_map.json, located in your assets folder. For each rectangle, it creates a WorldCollidable
and adds it to your game.
Call your new function beneath add(_player)
in onLoad
:
addWorldCollision();
Add the HasCollidables
mixin to RayWorldGame
. You’ll need to specify this if you want Flame to build a game that has collidable sprites:
with HasCollidables
Add the imports for map_loader
, world_collidable
and components
at the top of your file:
import 'components/world_collidable.dart';
import 'helpers/map_loader.dart';
import 'package:flame/components.dart';
You’ve now added all your collidable sprites into the game, but right now, you won’t be able to tell. You’ll need to incorporate additional logic to your player to stop them from moving when they’ve collided with one of these objects.
Start by going to your Player
class and adding the Hitbox
and Collidable
mixins after with HasGameRef
next to your player class declaration:
class Player extends SpriteAnimationComponent
with HasGameRef, Hitbox, Collidable
You now have access to onCollision
and onCollisionEnd
. Add them to your Player
class:
@override
void onCollision(Set<Vector2> intersectionPoints, Collidable other) {
// TODO 1
}
@override
void onCollisionEnd(Collidable other) {
// TODO 2
}
Create and add a HitboxRectangle
to your Player
in the constructor. Like your WorldCollision
components, your player needs a Hitbox
to be able to register collisions:
Player()
: super(
size: Vector2.all(50.0),
) {
addHitbox(HitboxRectangle());
}
Add the geometry and WorldCollidable
imports above your class:
import 'package:flame/geometry.dart';
import 'world_collidable.dart';
Now, add two variables into your Player
class to help track your collisions:
Direction _collisionDirection = Direction.none;
bool _hasCollided = false;
You can populate these variables in the two collision methods. Go to onCollision
and replace // TODO 1
with logic to collect collision information:
if (other is WorldCollidable) {
if (!_hasCollided) {
_hasCollided = true;
_collisionDirection = direction;
}
}
Set _hasCollided
back to false in onCollisionEnd
, replacing // TODO 2
:
_hasCollided = false;
Player
now has all the information it needs to know whether it has collided or not. You can use that information to prohibit movement. Add these four methods to your Player
class:
bool canPlayerMoveUp() {
if (_hasCollided && _collisionDirection == Direction.up) {
return false;
}
return true;
}
bool canPlayerMoveDown() {
if (_hasCollided && _collisionDirection == Direction.down) {
return false;
}
return true;
}
bool canPlayerMoveLeft() {
if (_hasCollided && _collisionDirection == Direction.left) {
return false;
}
return true;
}
bool canPlayerMoveRight() {
if (_hasCollided && _collisionDirection == Direction.right) {
return false;
}
return true;
}
These methods will check whether the player can move in a given direction by querying the collision variables you created. Now, you can use these methods in movePlayer
to see whether the player should move:
void movePlayer(double delta) {
switch (direction) {
case Direction.up:
if (canPlayerMoveUp()) {
animation = _runUpAnimation;
moveUp(delta);
}
break;
case Direction.down:
if (canPlayerMoveDown()) {
animation = _runDownAnimation;
moveDown(delta);
}
break;
case Direction.left:
if (canPlayerMoveLeft()) {
animation = _runLeftAnimation;
moveLeft(delta);
}
break;
case Direction.right:
if (canPlayerMoveRight()) {
animation = _runRightAnimation;
moveRight(delta);
}
break;
case Direction.none:
animation = _standingAnimation;
break;
}
}
Rebuild your game and try to run to the water’s edge or into a fence. You’ll notice your player will still animate, but you won’t be able to move past the collision objects. Try running between the fences or barrels.