Our Biggest Black Friday Sale — Ever!

Introducing unlimited access to all courses, all books, and our new monthly live professional development series! Just $899 $399 per year during our Black Friday event

Ends in... ::
Home Flutter Tutorials

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.

4.7/5 11 Ratings

Version

  • Dart 2.13, Flutter 2.2, VS Code

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.
Note: This tutorial assumes you have basic knowledge of Flutter. If you’re new to Flutter, check out Flutter Apprentice, or the Getting Started with Flutter tutorial. At the very least, you should know how to open a project in your favorite IDE, navigate the source code, initialize your packages with pub get and run your app in a simulator.

Getting Started

You’ll develop a game called RayWorld, a 2-D orthographic game in the style of old-school Pokemon.

RayWorld flame full game, with character running around the screen

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:

RayWorld flame screenshot 1

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.

RayWorld flame player standing

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:

2500x2500 grid

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.

RayWorld flame player movement with no animation gif

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:

RayWorld flame player sprite sheet

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.

RayWorld player sprite sheet with boxes

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.

RayWorld player movement gif

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:

RayWorld background

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.

RayWorld panning camera

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:

RayWorld collision map

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.

RayWorld collision gif

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!

More like this

Contributors

Comments