This is the second part of my Platformer 101 with Flame Engine series. In the previous part, we learned how to create a Flame game project, load assets, and make our character, The Boy, run. However, for now, it’s an empty screen, so let’s fix that in this part. We’re gonna design a game level, add camera movement, and parallax background.
If you remember from the last part, we store our game assets as a set of tiles, each of them representing a single cell in the game world. Now we need to design the level itself, telling the game where to draw ground, platforms, or other game objects. So we need a place that stores that information.
It could be as easy as an array of integers, that stores grid position and tile type. But usually games store level information in separate files, using JSON, XML, or even custom format - then the game parses the level file and translates it to game objects. Then you can go even further and create a level editor, that understands the level format you picked, so it’d be very easy to create new levels and modify existing ones.
Luckily, such tools already exist, one of the most prominent ones is Tiled - UI level editor with its own format, based on XML. It supports a lot of game engines, including Flame. Let’s download Tiled and create our game map.
After it’s downloaded, click New Map. These are the settings for my map. One thing: it’s better to use tiles of target size, as if you want to scale the map in the Flame itself, it might end up being blurry.
Next, click New tileset, which will store tiles used in our map.
Save both files to the assets/tiles
folder of your project.
Next, drag ground.png
and platform.png
into your tileset and then open the map tab. Using tiles from the tileset draw the level how you like it. Mine looks like this:
Don’t worry about the gray background, we’ll add it later in the game code. Save changes, and return to the IDE.
Now we need to tell the game to load the level we just created. First, open the pubspec.yaml
. We need to do two things here: to include flame_tiled
dependency and specify the path to our level in the assets section. Then press the pub get
button.
dependencies:
flutter:
sdk: flutter
flame: ^1.6.0
flame_tiled: ^1.9.1
flutter:
assets:
- assets/images/
- assets/tiles/
Then go to game.dart
file and load our level file in onLoad
method, before adding TheBoy
component:
final level = await TiledComponent.load("level1.tmx", Vector2.all(64));
mapWidth = level.tileMap.map.width * level.tileMap.destTileSize.x;
mapHeight = level.tileMap.map.height * level.tileMap.destTileSize.y;
add(level);
The first param is the name of the level we want to load and the second is the size of a single cell in our game grid. You can put any value here, but again if you set a value bigger than the size of your tiles assets, they will be scaled and might not look very crispy. It’s better to have the tiles in the desired size.
Also, create two class variables mapWidth
and mapHeight
that represents the size of our level. We’ll use these variables later.
If you run the game now, depending on your map setup you probably won’t see anything except the player character. That’s because the game size is now much bigger due to the added TiledComponent
, and you don’t see it completely. It could be fixed by setting a viewport of the game camera. Wait, what's a camera?
Imagine a Super Mario bros level. When the character reaches a certain point, the screen scrolls as if the camera was following the player. That’s exactly it: the game level could be any size, but we need to show only a small portion of it and the viewport is responsible for telling the game what portion of our level we want to show.
So let’s add this to the bottom of our onLoad
method:
camera.viewport = FixedResolutionViewport(Vector2(1920 , 1280));
If you run the game you should see this. We put this resolution intentionally, to see the whole level for now.
Now let’s move The Boy’s position to the left corner of our level.
final theBoy = TheBoy(
position: Vector2(128, mapHeight - 64),
);
add(theBoy);
Next, let’s zoom our camera a little bit, so the player won’t see the whole level at once:
camera.zoom = 2;
Finally, let’s make our camera move the player character so that when they reach the side of the screen, the level scrolls.
camera.followComponent(theBoy, worldBounds: Rect.fromLTWH(0, 0, mapWidth, mapHeight));
The second parameter worldBounds
will stop our camera from moving beyond level bounds when the player reaches the edge of the level.
The last thing we want to do is to restrict The Boy from moving away from the screen. It can be easily done, by setting the x velocity to 0, if our TheBoy
component’s position is around the edge of the screen. Let’s add two methods, to check for that:
bool doesReachLeftEdge() {
return position.x <= size.x / 2 && _horizontalDirection < 0;
}
bool doesReachRightEdge() {
return position.x >= game.mapWidth - size.x / 2 && _horizontalDirection > 0;
}
Easy enough, we check if the player is facing the edge and if their x position is the same as the edge coordinate. One thing, we need to account for the sprite size, otherwise, the player will be able to move out of the screen by the half-sprite size distance (because the sprite’s anchor point is in the center).
Now, we just restrict the movement, if the player has reached one of the screen edges:
@override
void update(double dt) {
super.update(dt);
if (doesReachLeftEdge() || doesReachRightEdge()) {
_velocity.x = 0;
} else {
_velocity.x = _horizontalDirection * _moveSpeed;
}
...
}
Awesome, our game looks a lot better! But we can make it even better by adding a nice background.
What makes side-scrolling game graphics really stand out is a parallax background. It’s a technique, when objects in the foreground move faster than the objects in the background, creating an illusion of depth. Luckily Flame has tools to easily add a parallax background to your game.
Let’s create a new file background.dart
in lib/objects
folder:
class ParallaxBackground extends ParallaxComponent<PlatformerGame> {
ParallaxBackground({required super.size});
@override
Future<void> onLoad() async {
final clouds = await game.loadParallaxLayer(
ParallaxImageData(Assets.CLOUDS),
velocityMultiplier: Vector2(1, 0),
fill: LayerFill.none,
alignment: Alignment.topCenter,
);
final mist = await game.loadParallaxLayer(
ParallaxImageData(Assets.MIST),
velocityMultiplier: Vector2(2, 0),
fill: LayerFill.none,
alignment: Alignment.bottomCenter,
);
final hills = await game.loadParallaxLayer(
ParallaxImageData(Assets.HILLS),
velocityMultiplier: Vector2(3, 0),
fill: LayerFill.none,
alignment: Alignment.bottomCenter,
);
positionType = PositionType.viewport;
parallax = Parallax(
[clouds, mist, hills],
baseVelocity: Vector2.all(10),
);
}
}
Here, we’re creating three parallax layers that will be moving at different speeds. There are several parameters that we can configure each layer with.
velocityMultiplier
- how fast the layer will be moving compared to the baseVelocity
of the ParallaxComponent
. The further the layer from the foreground, the higher the value should be.
fill
- if we want to scale the sprite to fill the viewport
alignment
- position of the layer on the game screen. With the current version of Flame, you only have options to attach the layer to the top, center, or bottom of the screen. If you want more fine-tuning, you need to add paddings to the asset itself. Another option would be to have several nested ParallaxComponents
.
With that done, let’s add our ParallaxBackground
component to the game tree, before other components:
add(ParallaxBackground(size: Vector2(mapWidth, mapHeight)));
And also, let’s override backgroundColor
method in game.dart
, to have a nicer background color:
@override
Color backgroundColor() {
return const Color.fromARGB(255, 69, 186, 230);
}
Much better, but currently the background moves independently of the player’s movement. But we want the parallax background to move with the camera. Let’s fix it! Return to the background.dart
and add a new class variable:
Vector2 _lastCameraPosition = Vector2.zero();
Next, set baseVelocity
param value in Parallax
object constructor to Vector2.zero()
Finally, add the implementation of update
method:
@override
void update(double dt) {
final cameraPosition = gameRef.camera.position;
final baseVelocity = (cameraPosition - _lastCameraPosition) * 10;
parallax!.baseVelocity.setFrom(baseVelocity);
_lastCameraPosition.setFrom(gameRef.camera.position);
super.update(dt);
}
So, instead of the constant baseVelocity
we want to calculate it dynamically, based on the camera movement. We calculate the difference in camera position since the last game loop and multiply it by the base velocity rate (in this instance 10). Then we update baseVelocity
with the calculated value. Let’s test it.
That’s what we wanted. The background doesn’t move on its own, but when the camera moves, the background moves with it, each layer with its own speed, creating a nice parallax effect.
That’s the end of Part 2. We learned how to create a level map using the Tiled editor, made the camera follow the player’s character, and added awesome parallax background. All the game code we have at this point can be found on my GitHub and the first story of the series is here.
Now our project looks like a real game. But in the next chapter, we’ll make it even better, by teaching The Boy how to jump.
At the end of each part, I’ll be adding a list of awesome creators and resources I learned from.