visit
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.
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.
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.
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();
}
Let’s go to theboy.dart
and add a hitbox for him as well to the end of onLoad
:
add(CircleHitbox());
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:
@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.
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
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;
}
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);
}
if (_hasJumped) {
if (_standingOn != null) {
_velocity.y = -_jumpSpeed;
}
_hasJumped = false;
}
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.
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.