This is Part 3 of the series, where I learn to create a simple platformer game using
Flame Engine.
In the last parts (1, 2), we created a game level with a parallax background, added a player character, The Boy, and taught him to run. Of course, any platformer would be boring without jumping, so in this part, we’re gonna add vertical movement with the help of collision detection.
The first thing we need to do is to add the force that constantly pulls the player to the ground.
I need to say, that if your game requires complex physics, it might be worth exploring Forge2D - the physics engine module for Flame. It deserves a series of its own, so to keep it simple in this series we’ll replicate gravity with just the means of Flame.
Add several new constants to the top of the TheBoy
component:
final double _gravity = 15; // How fast The Boy gets pull down
final double _jumpSpeed = 500; // How high The Boy jumps
final double _maxGravitySpeed = 300; // Max speed The Boy can have when falling
Now, let’s apply it to the player’s y velocity by adding these two lines to the update
method, right after setting velocity.x
:
_velocity.y += _gravity;
_velocity.y = _velocity.y.clamp(-_jumpSpeed, _maxGravitySpeed);
The first line increments the vertical velocity of The Boy each game cycle by the _gravity
amount. The second line limits the velocity to be between the _jumpSpeed
and _maxGravitySpeed
in order to avoid unlimited acceleration.
If we run the game now, we’ll see that The Boy falls through the ground. At this point, it’s the expected behavior, because the game doesn’t know yet, that the ground should be solid. For the game engine, our level is just a set of sprites, and we need to tell it, which tiles are the platforms. Luckily, it could be done, with the Tiled editor, which we used to create the level in Part 2.
Return to the Tiled. As you may have noticed, a Tiled map has different layers, similar to Adobe Photoshop. We already have a Tile layer, that contains tiles for our level. Now, we’ll add a new Object layer, which will represent the boundaries of our platforms. Name it “Platforms”
Now, select this layer, and using the Rectangle tool, draw rectangles where you placed your platforms and ground tiles. Once you’re finished, if you hide your Tile layer, it should be like this:
What we did, is we added the “invisible” layer, that holds the information on the position of the platforms. Next, we’ll use the game engine to translate those into actual components, that we can later use for collision detection. Save the level file and return to the IDE.
Let’s add the component that represents the platform to the objects
folder:
class Platform extends PositionComponent {
Platform(Vector2 position, Vector2 size) : super(position: position, size: size);
@override
Future<void> onLoad() async {
return super.onLoad();
}
}
Now, go to the game.dart
and add a new method:
void spawnObjects(RenderableTiledMap tileMap) {
final platforms = tileMap.getLayer<ObjectGroup>("Platforms");
for (final platform in platforms!.objects) {
add(Platform(Vector2(platform.x, platform.y), Vector2(platform.width, platform.height)));
}
}
And call it from the onLoad
after we added our level component:
spawnObjects(level.tileMap);
Let’s go through what happened here. We took our tilemap object and fetched the layer we just created by its name. Be aware that the key here should exactly match the layer name in Tiled: tileMap.getLayer<ObjectGroup>("Platforms")
.
Then, we iterate through all the objects in this layer, and for each of them create a Platform
component with the same position and size.
Now our game is populated with Platforms
, that correspond to the platform tiles the player sees. However, they do nothing for now, and for them to work we need to add collision detection.
Collision detection is a very common problem in game dev. Essentially, it means that when two game objects intersect, we want to trigger some events. For example, a bullet hits the target, two cars crash into each other, or The Boy lands on a platform.
The Flame engine provides a useful API to handle collision detection. First, let’s do some prep work. Open game.dart
and add HasCollisionDetection
mixin to tell the engine that we want to track collisions:
class PlatformerGame extends FlameGame with HasKeyboardHandlerComponents, HasCollisionDetection
Then, go to the Platform
class and add a hitbox, the component’s area we want to use for collision detection:
@override
Future<void> onLoad() async {
add(RectangleHitbox()..collisionType = CollisionType.passive);
return super.onLoad();
}
There’re three types of collisions:
Since platforms will only collide with The Boy, their type is passive.
Let’s go to theboy.dart
and add a hitbox for him as well to the end of onLoad
:
add(CircleHitbox());
Here we’re using a Circle hitbox because it fits the shape of the character better, but also it’d be easier to detect the collision for this shape. But more about it later.
Add the CollisionCallbacks
mixin to TheBoy
:
class TheBoyPlayer extends SpriteAnimationComponent
with KeyboardHandler, CollisionCallbacks, HasGameRef<PlatformerGame>
And override the method onCollision
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other)
This method will be triggered every time TheBoy
component collides with another component.
The first param is the points where two components intersect, and the second param is the component TheBoy
collides with.
The collision handling method I will use is the one described in the DevKage’s tutorial (link at the end of the story) with slight improvements. I should mention, that there are a bunch of ways a collision could be resolved. I’m gonna use a rather simple one that does a decent job, but of course, it could be improved further.
The collision of the player’s circle hitbox and the platforms’ rectangular hitbox at the moment onCollision
is called, could be schematized like this:
The problem here is that hitboxes intersect and in each game loop, the player will continue to sink into the platform. To avoid that, we need to calculate how deep the circle hitbox sank into the rectangular and move it up the same amount of pixels. That will create the illusion of the player standing on the platform.
We know the circle radius, and intersection points, so we can calculate the penetration depth:
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is Platform) {
if (intersectionPoints.length == 2) {
final mid = (intersectionPoints.elementAt(0) +
intersectionPoints.elementAt(1)) / 2;
final collisionVector = absoluteCenter - mid;
double penetrationDepth = (size.x / 2) - collisionVector.length;
collisionVector.normalize();
position += collisionVector.scaled(penetrationDepth);
}
}
super.onCollision(intersectionPoints, other);
}
First, we calculate the middle point between the points of intersection (mid
). Then, using the circle center and mid
we calculate the collision vector - the direction The Boy moves towards the platform. Next, using the radius (size.x / 2
) and length of collisionVector
we calculate how deep the circle hitbox is penetrating the rectangular platform. Finally, we normalize the vector to have just the direction of the collision and update the player’s position by penetrationDepth
multiplied by the normalized vector to keep the direction.
If we run the game, The Boy doesn’t fall through the platform anymore. And if he moves forward, the wall stops him.
Let’s add jumping. First, we need to detect if the arrow key was pressed. Add a new variable to the TheBoy
class:
bool _hasJumped = false;
Modify the onKeyEvent
method and add the following to the bottom:
_hasJumped = keysPressed.contains(LogicalKeyboardKey.keyW) || keysPressed.contains(LogicalKeyboardKey.arrowUp);
Next, edit the update
method and add this right after applying gravity:
if (_hasJumped) {
_velocity.y = -_jumpSpeed;
_hasJumped = false;
}
Here we check if the arrow key was pressed, and update The Boy’s y velocity by the _jumpSpeed
If you run the game now, you’ll see that The Boy learned how to jump! The only problem is that if you press the arrow key mid-jump, he will jump again. We need to fix that.
We’re gonna return to the onCollision
method and save the component The Boy is standing on. Then, when the collision ends, we’re gonna clear this reference. Lastly, we’re gonna check if the reference is not empty before applying our jump logic. This will allow The Boy to jump only when there’s an active collision with the ground.
Add these variables to the TheBoy
class:
Component? _standingOn; // The component The Boy is currently standing on
final Vector2 up = Vector2(0, -1); // Up direction vector we're gonna use to determine if The Boy is on the ground
final Vector2 down = Vector2(0, 1); // Down direction vector we're gonna use to determine if The Boy hit the platform above
Go to onCollision
method and add the following right after collisionVector
normalization:
if (up.dot(collisionVector) > 0.9) {
_standingOn = other;
}
Here we check if The Boy is colliding with the platform below him and save the reference to that component.
Next, add onCollisionEnd
method implementation, which will be triggered every time The Boy stops colliding with a platform:
@override
void onCollisionEnd(PositionComponent other) {
if (other == _standingOn) {
_standingOn = null;
}
super.onCollisionEnd(other);
}
Finally, modify the jumping logic:
if (_hasJumped) {
if (_standingOn != null) {
_velocity.y = -_jumpSpeed;
}
_hasJumped = false;
}
Cool, The Boy should be allowed to jump only once now. But there’s one more improvement we can add. If you jump below a platform, you’ll notice that The Boy hangs for a second. To avoid that, we’re gonna check if The Boy is colliding with a platform on the top, similarly as we did with the ground. Then we’re gonna bounce The Boy by modifying his vertical velocity.
Add else branch to the condition we added earlier:
if (up.dot(collisionVector) > 0.9) {
_standingOn = other;
} else if (down.dot(collisionVector) > 0.9) {
_velocity.y += _gravity;
}
Well done! Now The Boy can jump and fall.
One little improvement we can add, that greatly improves the look of the game is using different animations for jumps. Let’s do that.
Add _jumpAnimation
and _fallAnimation
to onLoad
method, similar to what we’ve done for idle and run animations:
_jumpAnimation = SpriteAnimation.fromFrameData(
game.images.fromCache(Assets.THE_BOY),
SpriteAnimationData.range(
start: 4,
end: 4,
amount: 6,
textureSize: Vector2.all(20),
stepTimes: [0.12],
),
);
_fallAnimation = SpriteAnimation.fromFrameData(
game.images.fromCache(Assets.THE_BOY),
SpriteAnimationData.range(
start: 5,
end: 5,
amount: 6,
textureSize: Vector2.all(20),
stepTimes: [0.12],
),
);
Then, let’s modify updateAnimation
method to include new animations:
void updateAnimation() {
if (_standingOn != null) {
if (_horizontalDirection == 0) {
animation = _idleAnimation;
} else {
animation = _runAnimation;
}
} else {
if (_velocity.y > 0) {
animation = _fallAnimation;
} else {
animation = _jumpAnimation;
}
}
}
Here, we check if The Boy is standing on the ground (_standingOn != null
), then we use the run or the idle animation. Otherwise, we check if The Boy jumps or falls by checking the sign of the vertical velocity and then we apply the appropriate animation.
Awesome! The animations look much better.
That’s it for Part 3. We learned how to add simple physics and collision detection. The Boy now can jump and the game is starting to take its final shape. In the last part, we’ll add Coins - another type of object The Boy can interact with. And also we’ll add a HUD to display the collected Coins.
Other stories of the series:
The complete code for this part you can find on my github
At the end of each part, I’ll be adding a list of awesome creators and resources I learned from.
DevKage’s Flame Game Development Series:
https://www.youtube.com/watch?v=mSPalRqZQS8&feature=youtu.be
Craig Oda’s channel: