5. Controlling Movement¶
If you were waiting for some serious coding, this chapter is it. Prepare yourself as we dive in!
Keyboard Controls¶
The first step will be to allow control of Ember via the keyboard. We need to start by adding the appropriate mixins to the game class and Ember. Add the following:
lib/ember_quest.dart
import 'package:flame/events.dart';
class EmberQuestGame extends FlameGame with HasKeyboardHandlerComponents {
lib/actors/ember.dart
class EmberPlayer extends SpriteAnimationComponent
with KeyboardHandler, HasGameReference<EmberQuestGame> {
Now we can add a new method:
@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
return true;
}
Like before, if this did not trigger an auto-import, you will need the following:
import 'package:flutter/services.dart';
To control Ember’s movement, it is easiest to set a variable where we think of the direction of movement like a normalized vector, meaning the value will be restricted to -1, 0, or 1. So let’s set a variable at the top of the class:
int horizontalDirection = 0;
Now in our onKeyEvent
method, we can register the key pressed by adding:
@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
horizontalDirection = 0;
horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyA) ||
keysPressed.contains(LogicalKeyboardKey.arrowLeft))
? -1
: 0;
horizontalDirection += (keysPressed.contains(LogicalKeyboardKey.keyD) ||
keysPressed.contains(LogicalKeyboardKey.arrowRight))
? 1
: 0;
return true;
}
Let’s make Ember move by adding a few lines of code and creating our update
method. First, we
need to define a velocity variable for Ember. Add the following at the top of the EmberPlayer
class:
final Vector2 velocity = Vector2.zero();
final double moveSpeed = 200;
This establishes a base velocity of 0 and stores moveSpeed
so we can adjust as necessary to suit
how the game-play should be. Next, add the update
method with the following:
@override
void update(double dt) {
velocity.x = horizontalDirection * moveSpeed;
position += velocity * dt;
super.update(dt);
}
If you run the game now, Ember moves left and right using the arrow keys or the A
and D
keys.
You may have noticed that Ember doesn’t look back if you are going left, to fix that, add the
following code at the end of your update
method:
if (horizontalDirection < 0 && scale.x > 0) {
flipHorizontally();
} else if (horizontalDirection > 0 && scale.x < 0) {
flipHorizontally();
}
Now Ember looks in the direction they are traveling.
Collisions¶
It is time to get into the thick of it with collisions. I highly suggest reading the
documentation to understand how collisions work in Flame. The
first thing we need to do is make the game aware that collisions are going to occur using the
HasCollisionDetection
mixin. Add that to lib/ember_quest.dart
like:
class EmberQuestGame extends FlameGame
with HasCollisionDetection, HasKeyboardHandlerComponents {
Next, add the CollisionCallbacks
mixin to lib/actors/ember.dart
like:
class EmberPlayer extends SpriteAnimationComponent
with KeyboardHandler, CollisionCallbacks, HasGameReference<EmberQuestGame> {
If it did not auto-import, you will need the following:
import 'package:flame/collisions.dart';
Now add the following onCollision
method:
@override
void onCollision(Set<Vector2> intersectionPoints, PositionComponent other) {
if (other is GroundBlock || other is PlatformBlock) {
if (intersectionPoints.length == 2) {
// Calculate the collision normal and separation distance.
final mid = (intersectionPoints.elementAt(0) +
intersectionPoints.elementAt(1)) / 2;
final collisionNormal = absoluteCenter - mid;
final separationDistance = (size.x / 2) - collisionNormal.length;
collisionNormal.normalize();
// If collision normal is almost upwards,
// ember must be on ground.
if (fromAbove.dot(collisionNormal) > 0.9) {
isOnGround = true;
}
// Resolve collision by moving ember along
// collision normal by separation distance.
position += collisionNormal.scaled(separationDistance);
}
}
super.onCollision(intersectionPoints, other);
}
You will need to import the following:
import '../objects/ground_block.dart';
import '../objects/platform_block.dart';
As well as create these class variables:
final Vector2 fromAbove = Vector2(0, -1);
bool isOnGround = false;
For the collisions to be activated for Ember, we need to add a CircleHitbox
, so in the onLoad
method, add the following:
add(CircleHitbox());
Now that we have the basic collisions created, we can add gravity so Ember exists in a game world with very basic physics. To do that, we need to create some more variables:
final double gravity = 15;
final double jumpSpeed = 600;
final double terminalVelocity = 150;
bool hasJumped = false;
Now we can add Ember’s ability to jump by adding the following to our onKeyEvent
method:
hasJumped = keysPressed.contains(LogicalKeyboardKey.space);
Finally, in our update
method we can tie this all together with:
// Apply basic gravity
velocity.y += gravity;
// Determine if ember has jumped
if (hasJumped) {
if (isOnGround) {
velocity.y = -jumpSpeed;
isOnGround = false;
}
hasJumped = false;
}
// Prevent ember from jumping to crazy fast as well as descending too fast and
// crashing through the ground or a platform.
velocity.y = velocity.y.clamp(-jumpSpeed, terminalVelocity);
Earlier I mentioned that Ember was in the center of the grass, to solve this and show how collisions
and gravity work with Ember, I like to add a little drop-in when you start the game. So in
lib/ember_quest.dart
in the initializeGame
method, change the following:
_ember = EmberPlayer(
position: Vector2(128, canvasSize.y - 128),
);
If you run the game now, Ember should be created and fall to the ground; then you can jump around!
Collisions with Objects¶
Adding the collisions with the other objects is fairly trivial. All we need to do is add the
following to the bottom of the onCollision
method:
if (other is Star) {
other.removeFromParent();
}
if (other is WaterEnemy) {
hit();
}
When Ember collides with a star, the game will remove the star, and to implement the hit
method for
when Ember collides with an enemy, we need to do the following:
Add the following variable at the top of the EmberPlayer
class:
bool hitByEnemy = false;
Additionally, add this method to the EmberPlayer
class:
// This method runs an opacity effect on ember
// to make it blink.
void hit() {
if (!hitByEnemy) {
hitByEnemy = true;
}
add(
OpacityEffect.fadeOut(
EffectController(
alternate: true,
duration: 0.1,
repeatCount: 6,
),
)..onComplete = () {
hitByEnemy = false;
},
);
}
If the auto-imports did not occur, you will need to add the following imports to your file:
import 'package:flame/effects.dart';
import '../objects/star.dart';
import 'water_enemy.dart';
If you run the game now, you should be able to move around, make stars disappear, and if you collide with an enemy, Ember should blink.
Adding the Scrolling¶
This is our last task with Ember. We need to restrict Ember’s movement because as of now, Ember can
go off-screen and we never move the map. So to implement this feature, we simply need to add the
following to the end of our update
method:
game.objectSpeed = 0;
// Prevent ember from going backwards at screen edge.
if (position.x - 36 <= 0 && horizontalDirection < 0) {
velocity.x = 0;
}
// Prevent ember from going beyond half screen.
if (position.x + 64 >= game.size.x / 2 && horizontalDirection > 0) {
velocity.x = 0;
game.objectSpeed = -moveSpeed;
}
position += velocity * dt;
super.update(dt);
If you run the game now, Ember can’t move off-screen to the left, and as Ember moves to the right,
once they get to the middle of the screen, the rest of the objects scroll by. This is because we
are now updating game.objectSpeed
which we established early on in the series. Additionally,
you will see the next random segment be generated and added to the level based on the work we did in
Ground Block.
Note
As I mentioned earlier, I would add a section on how this game could be adapted
to a traditional level game. As we built the segments in 3. Building the World, we
could add a segment that has a door or a special block. For every X
number of
segments loaded, we could then add that special segment. When Ember reaches that
object, we could reload the level and start all over maintaining the stars
collected and health.
We are almost done! In 6. Adding the HUD, we will add the health system, keep track of the score, and provide a HUD to relay that information to the player.