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?
Flutter is changing the world by bringing fast, natively compiled software to the masses. This allows indie developers to release applications for every platform in the same time it would usually have taken a software company. It’s only natural that game developers want to take advantage of that, too.
Traditionally, a mobile game developer would need to choose between native performance but slow development time or building with a multi-platform tool like Unity but risk slow loading times and large app sizes.
If only there were a way to develop beautiful native games without all the bloat. That’s where Flame comes in.
Today, you’ll build a virtual world using Flutter and the Flame engine. You’ll learn how to:
- Use Flame version 1.0 to make a game for the web, Android and iOS.
- Use a game loop.
- Create a movable player character.
- Animate your character with sprite sheets.
- Add box collision by reading from a tile map.
Getting Started
You’ll develop a game called RayWorld, a 2-D orthographic game in the style of old-school Pokemon.
Using an older game engine written in something like C++, a tutorial like this would span over three or four series. But with the power of Flutter and the Flame engine combined, you’ll create all this in just one.
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.
Build and run your project in your preferred IDE. This tutorial will use Visual Studio Code.
You’ll see a blank screen with a joypad in the bottom right corner:
What you see here is rendered purely with Flutter; you’ll need Flame to build the rest of your components.
The Flame Game Engine
Flame — a lightweight game engine built on top of Flutter — gives game developers a set of tools such as a game loop, collision detection and sprite animations to create 2-D games.
The Flame team has worked on releasing v1.0.0 for over a year and is on the brink of an official release. v1.0.0 has changed a lot of the core fundamentals of Flame because the team took this opportunity to apply what they’ve learned over the years and rewrite the engine.
This tutorial will use Flame 1.0.0 release candidate 15.
The Flame engine is modular, allowing users to pick and choose which API’s they would like to use, such as:
- Flame – The core package, which offers the game loop, basic collision detection, Sprites and components.
- Forge2D – A physics engine with advanced collision detection, ported from Box2D to work with Flame.
- Tiled – A module for easily working with tile maps in Flame.
- Audio – A module that adds audio capabilities into your Flame game.
Flame harnesses the power of Flutter and provides a lightweight approach to developing 2-D games for all platforms.
Setting up Your Flame Game Loop
The first component you’ll set up in RayWorld is your Flame game loop. This will be the heart of your game. You’ll create and manage all your other components from here.
Open your lib folder and create a new file called ray_world_game.dart, then add a new class called RayWorldGame
, which extends from the Flame widget FlameGame
:
import 'package:flame/game.dart';
class RayWorldGame extends FlameGame {
@override
Future<void> onLoad() async {
// empty
}
}
Open main_game_page.dart and create an instance of your new class at the top of MainGameState
:
RayWorldGame game = RayWorldGame();
Now, add a GameWidget
to MainGameState
as the first widget in the Stack
, replacing // TODO 1
with:
GameWidget(game: game),
Add these two imports to the top of main_game_page.dart so you can use your new logic:
import 'package:flame/game.dart';
import 'ray_world_game.dart';
Right now, your game will do nothing. It needs some components to render. Time to add a playable character!
Creating Your Player
Add a folder in lib called components. This folder will store all your Flame components, starting with your player.
Create a file in components called player.dart. In this class, set up your Player
class:
import 'package:flame/components.dart';
class Player extends SpriteComponent with HasGameRef {
Player()
: super(
size: Vector2.all(50.0),
);
@override
Future<void> onLoad() async {
super.onLoad();
// TODO 1
}
}
Your Player
extends a Flame component called SpriteComponent
. You’ll use this to render a static image in your game. You’re setting the size of the player to be 50.
All components in the Flame engine have some core functionality, such as loading and rendering within the game loop they’re attached to. For now, you’ll use only onLoad
.
Replace // TODO 1
in Player
with logic to load your player image and set the player’s initial position.
sprite = await gameRef.loadSprite('player.png');
position = gameRef.size / 2;
All components have access to the game loop if you add the mixin HasGameRef
to your component class definition. Here, you use that game reference to load a sprite into your game with the image of player.png that’s located in your Flutter assets folder. You also set the players position to be in the middle of the game.
Go back to your ray_world_game.dart file and add your new Player
component as an import at the top of the file:
import 'components/player.dart';
In the top of RayWorldGame
, create your Player
:
final Player _player = Player();
In the game onLoad
method, replace // empty
with code to add your player into the game:
add(_player);
add
is a super important method when building games with the Flame engine. It allows you to register any component with the core game loop and ultimately render them on screen. You can use it to add players, enemies, and lots of other things as well.
Build and run, and you’ll see a little dude standing in the center of your game.
Pretty exciting!
Now, it’s time to get your player moving.
Adding Movement to Your Player
To move your player, you first need to know what direction the joypad is dragged.
The joypad direction is retrieved from the Joypad
Flutter widget that lives outside the game loop. The direction then gets passed to the GameWidget
in main_game_page.dart. In turn, this can pass it to Player
, which can react to the direction change with movement.
Start with the Player
.
Open your player.dart
file and add the import for direction:
import '../helpers/direction.dart';
Then, declare a Direction
variable in the top of Player
and instantiate it to Direction.none
:
Direction direction = Direction.none;
The joypad will change to either up, down, left, right, or none. With each new position, you want to update the direction
variable.
Open ray_world_game.dart
and add a function to update the direction of your player in RayWorldGame
:
void onJoypadDirectionChanged(Direction direction) {
_player.direction = direction;
}
Also add the direction import to the top of ray_world_game.dart
:
import '../helpers/direction.dart';
Now, head back to main_game_page.dart
and replace // TODO 2
with a call to your game direction function:
game.onJoypadDirectionChanged(direction);
And voilà, you’ve passed a user input from a Flutter widget into your game and player components.
Now that your player component knows what direction it should be moving in, it’s time to execute on that information and actually move your player!
Executing on Player Movement
To start acting on the information passed through to the player component, head back to player.dart and add these two functions:
@override
void update(double delta) {
super.update(delta);
movePlayer(delta);
}
void movePlayer(double delta) {
// TODO
}
update
is a function unique to Flame components. It will be called each time a frame must be rendered, and Flame will ensure all your game components update at the same time. The delta represents how much time has passed since the last update cycle and can be used to move the player predictably.
Replace // TODO
in the movePlayer
function with logic to read the direction:
switch (direction) {
case Direction.up:
moveUp(delta);
break;
case Direction.down:
moveDown(delta);
break;
case Direction.left:
moveLeft(delta);
break;
case Direction.right:
moveRight(delta);
break;
case Direction.none:
break;
}
movePlayer
will now delegate out to other more specific methods to move the player. Next, add the logic for moving the player in each direction.
Start by adding a speed variable to the top of your Player
class:
final double _playerSpeed = 300.0;
Now, add a moveDown
function to the bottom of your Player
class:
void moveDown(double delta) {
position.add(Vector2(0, delta * _playerSpeed));
}
Here, you update the Player
position value — represented as an X and a Y inside Vector2
— by your player speed multiplied by the delta.
You can picture your game view drawn on a 2-D plane like so:
If the game view is 2500×2500 pixels in diameter, your player starts in the middle at the coordinates of x:1250, y:1250. Calling moveDown
adds about 300 pixels to the player’s Y position each second the user holds the joypad in the down direction, causing the sprite to move down the game viewport.
You must add a similar calculation for the other three missing methods: moveUp
, moveLeft
and moveRight
.
See if you can add these methods yourself, thinking about how your sprite moves on a 2-D plane.
Need help? Just open the spoiler below.
[spoiler title=”Solution”]
void moveUp(double delta) {
position.add(Vector2(0, delta * -_playerSpeed));
}
void moveLeft(double delta) {
position.add(Vector2(delta * -_playerSpeed, 0));
}
void moveRight(double delta) {
position.add(Vector2(delta * _playerSpeed, 0));
}
[/spoiler]
Run your application once more, and your little dude will move around the screen in all directions based on your joypad input.
Animating Your Player
Your player is moving around the screen like a boss – but it looks a bit off because the player is always facing in the same direction! You’ll fix that next using sprite sheets.
What Is a Sprite Sheet?
A sprite sheet is a collection of sprites in a single image. Game developers have used them for a long time to save memory and ensure quick loading times. It’s much quicker to load one image instead of multiple images. Game engines like Flame can then load the sprite sheet and render only a section of the image.
You can also use sprite sheets for animations by lining sprites up next to each other in animation frames so they can easily be iterated over in the game loop.
This is the sprite sheet you’ll use for your playable character in RayWorld:
Each row is a different animation set and simulates moving left, right, up and down.
Adding Sprite Sheet Animations to Your Player
In player.dart, change your Player
class extension from SpriteComponent
to SpriteAnimationComponent
. With this new type of component, you’ll be able to set an active animation, which will run on your player Sprite.
Import the package sprite.dart. You’ll need this for setting up a SpriteSheet
:
import 'package:flame/sprite.dart';
Add these six new variables to your Player
class:
final double _animationSpeed = 0.15;
late final SpriteAnimation _runDownAnimation;
late final SpriteAnimation _runLeftAnimation;
late final SpriteAnimation _runUpAnimation;
late final SpriteAnimation _runRightAnimation;
late final SpriteAnimation _standingAnimation;
Replace the onLoad
method with new logic to load your animations:
@override
Future<void> onLoad() async {
_loadAnimations().then((_) => {animation = _standingAnimation});
}
_loadAnimations
will be an async call. This method waits for the animations to load and then sets the sprite’s first active animation to _standingAnimation
.
Create the _loadAnimations
method and instantiate your player SpriteSheet
:
Future<void> _loadAnimations() async {
final spriteSheet = SpriteSheet(
image: await gameRef.images.load('player_spritesheet.png'),
srcSize: Vector2(29.0, 32.0),
);
// TODO down animation
// TODO left animation
// TODO up animation
// TODO right animation
// TODO standing animation
}
This code loads a sprite sheet image from your Flutter assets folder that you saw previously.
The image is 116×128 pixels, and each frame is 29×32 pixels. The latter is what you’re setting the srcSize
SpriteSheet
parameter to. Flame will use these variables to create sprites from the different frames on your sprite sheet image.
Replace // TODO down animation
with logic to initialize _runDownAnimation
:
_runDownAnimation =
spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 4);
This code sets up an animation that loops across the first row of the player sprite sheet from the first frame until the fourth. It’s effectively a “while” loop that repeats from 0 until less than 4, where the sprite viewport moves in 32 pixel increments across 4 rows.
Using this logic, initialize the rest of your animation variables.
Need help? Reveal the spoiler below.
[spoiler title=”Solution”]
_runLeftAnimation =
spriteSheet.createAnimation(row: 1, stepTime: _animationSpeed, to: 4);
_runUpAnimation =
spriteSheet.createAnimation(row: 2, stepTime: _animationSpeed, to: 4);
_runRightAnimation =
spriteSheet.createAnimation(row: 3, stepTime: _animationSpeed, to: 4);
_standingAnimation =
spriteSheet.createAnimation(row: 0, stepTime: _animationSpeed, to: 1);
[/spoiler]
Update your movePlayer
function to assign the correct animations based on the player’s direction:
void movePlayer(double delta) {
switch (direction) {
case Direction.up:
animation = _runUpAnimation;
moveUp(delta);
break;
case Direction.down:
animation = _runDownAnimation;
moveDown(delta);
break;
case Direction.left:
animation = _runLeftAnimation;
moveLeft(delta);
break;
case Direction.right:
animation = _runRightAnimation;
moveRight(delta);
break;
case Direction.none:
animation = _standingAnimation;
break;
}
}
Build and run, and you’ll see your playable character has come to life as they run in each direction.
At this point, you have the fundamentals of a game in place: a playable character with user input and movement. The next step is to add a world for your player to move around in.
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.
Bonus Section: Keyboard Input
Because RayWorld is built with Flutter, it can also run as a web app. Generally, for web games, people want to use keyboard input instead of a joypad. Flame has an interface called KeyboardEvents you can override in your game object to receive notification of keyboard input events.
For this bonus section, you’ll listen for keyboard events for the up, down, left and right arrows, and use these events to set the player’s direction.
Start by adding the mixin KeyboardEvents
to the end of your RayWorldGame
class declaration, next to HasCollidables
.
Add the input import above RayWorldGame
:
import 'package:flame/input.dart';
Now, override the onKeyEvent method:
@override
KeyEventResult onKeyEvent(RawKeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
final isKeyDown = event is RawKeyDownEvent;
Direction? keyDirection = null;
// TODO 1
// TODO 2
return super.onKeyEvent(event, keysPressed);
}
Replace // TODO 1
with logic to read RawKeyEvent
and set the keyDirection
:
if (event.logicalKey == LogicalKeyboardKey.keyA) {
keyDirection = Direction.left;
} else if (event.logicalKey == LogicalKeyboardKey.keyD) {
keyDirection = Direction.right;
} else if (event.logicalKey == LogicalKeyboardKey.keyW) {
keyDirection = Direction.up;
} else if (event.logicalKey == LogicalKeyboardKey.keyS) {
keyDirection = Direction.down;
}
Here, you are listening for key changes with the keys W, A, S and D and setting the corresponding movement direction.
Now, replace // TODO 2
with logic to change the player’s direction:
if (isKeyDown && keyDirection != null) {
_player.direction = keyDirection;
} else if (_player.direction == keyDirection) {
_player.direction = Direction.none;
}
The player’s direction is being updated if a key is being pressed, and if a key is lifted the players direction is set to Direction.none
if it is the active direction.
Launch your game on the web or an emulator, and you’ll now be able to run around using the W, A, S and D keys on your keyboard.
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.
You now have all the tools to make a complete 2-D game using the Flame Engine. But why stop there? You could try adding:
- More game UI: Incorporate UI elements such as a player health bar, an attack button and a jump button. You could build these using a Flame component or a Flutter Widget.
- Enemies: Populate RayWorld with enemies such as goblins or aggressive animals that could attack your player.
- Different levels: Load new world sprites and tile maps into your game as the player leaves the area.
Check out the awesome-flame GitHub repository to see what games have already been developed using the Flame Engine and to read some other great Flame tutorials.
As Flame v1.0.0 edges closer to an official release, there’s sure to be plenty of new and exciting game development API’s that take advantage of the Flutter ecosystem. Stay tuned to raywenderlich.com for more great game development tutorials as the release is rolled out!