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.

4.7 (3) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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. Make sure to add the World import.

import 'components/world.dart';

Then add a World as a variable under Player:

final World _world = World();

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!

First, add the import for using a Rect variable at the top of the file. You’ll use this to calculate some bounds:

import 'dart:ui';

Now 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));

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/collisions.dart';
import 'package:flame/components.dart';
 
class WorldCollidable extends PositionComponent{
  WorldCollidable() {
    add(RectangleHitbox());
  }
}

Here you define a new class to contain your world. It’s a type of PositionComponent that represents a position on the screen. It’s meant to represent each collidable area (i.e., invisible walls) on the world map.

Open ray_world_game.dart. First add the following imports:

import 'components/world_collidable.dart';
import 'helpers/map_loader.dart';
import 'package:flame/components.dart';

Now 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:

await add(_world);
add(_player);
addWorldCollision(); // add

Now to register collision detection. Add the HasCollisionDetection mixin to RayWorldGame. You’ll need to specify this if you want Flame to build a game that has collidable sprites:

class RayWorldGame extends FlameGame with HasCollisionDetection

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.

Open player.dart. Add the CollisionCallbacks mixin after with HasGameRef next to your player class declaration:

class Player extends SpriteAnimationComponent with HasGameRef, CollisionCallbacks

You now have access to onCollision and onCollisionEnd. Add them to your Player class:

@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
  super.onCollision(intersectionPoints, other);
  // TODO 1
}
 
@override
void onCollisionEnd(PositionComponent other) {
  super.onCollisionEnd(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),
       ) {
   add(RectangleHitbox());
 }

Add the WorldCollidable import above your class:

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