collisions happen

Let’s create a game using Flutter Flame. Part 2: Handling keyboard events and collisions

Andrii Khrystian
6 min readJul 19, 2023

--

Welcome to the next part of our journey with Flutter Flame. In the previous part, we explored the basic components and lifecycle, providing a solid understanding of how everything works together. As a result, we created a simple “game” that we can now enhance by introducing controls.

result of part 1

Controlling Your Components

Now that we have a macOS game, let’s use the physical keyboard to control our square component. To achieve this, we need to override the onKeyEvent function within the KeyboardEvents mixin. This mixin is utilized in our Game class. Additionally, we'll extend the functionality of the SquareComponent to describe its movement.


class SquareGameComponent extends PositionComponent {
static const double speed = 20;

@override
void render(Canvas canvas) {
super.render(canvas);

final paint = Paint()..color = Colors.blue;
canvas.drawRect(position & size, paint);
}

@override
void update(double dt) {
super.update(dt);
position.x += speed * dt;
}

void moveLeft() {
position.x -= speed;
}

void moveRight() {
position.x += speed;
}

void moveUp() {
position.y -= speed;
}

void moveDown() {
position.y += speed;
}
}

In the updated code, we introduced four functions that will be utilized when overriding keyboard events. We then implemented the necessary logic in the onKeyEvent function to handle the keyboard input.

class SquareGame extends FlameGame with KeyboardEvents {
late SquareGameComponent square;

@override
Color backgroundColor() => Colors.redAccent.shade100;

@override
Future<void> onLoad() async {
square = SquareGameComponent();
square.position = Vector2(0, 100);
square.size = Vector2(100, 100);
add(square);
}

@override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
square.moveLeft();
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
square.moveRight();
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
square.moveUp();
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
square.moveDown();
}
}
return KeyEventResult.handled;
}
}

Here we have a RawKeyDownEvent which particularly means that user pressed the button, and also LogicalKeyboardKey class which keeps variables representing all keyboard keys. And finally we need to return KeyEventResult which is handled in our case. We need this KeyEventResult in case if we have different components handling KeyEvents. As we have only one component handling KeyEvents we can just return this constant.

Now that our code is ready, let’s compile and test our game.

It works :)

However, before we proceed, let’s enhance the movement of our square and introduce simple state management. For this purpose, we’ll add a simple enumeration that represents the movement direction.

enum MovingState {
still,
up,
down,
right,
left;
}

And code with enum:

class SquareGameComponent extends PositionComponent {
static const double speed = 20;
static const double movementSpeed = 80;
MovingState state = MovingState.still;

@override
void render(Canvas canvas) {
super.render(canvas);

final paint = Paint()..color = Colors.blue;
canvas.drawRect(position & size, paint);
}

@override
void update(double dt) {
super.update(dt);

position.x += speed * dt;

switch (state) {
case MovingState.left:
moveLeft(dt);
break;
case MovingState.right:
moveRight(dt);
break;
case MovingState.up:
moveUp(dt);
break;
case MovingState.down:
moveDown(dt);
break;
case MovingState.still:
break;
}
}

void moveLeft(double dt) {
position.x -= movementSpeed * dt;
}

void moveRight(double dt) {
position.x += movementSpeed * dt;
}

void moveUp(double dt) {
position.y -= movementSpeed * dt;
}

void moveDown(double dt) {
position.y += movementSpeed * dt;
}
}

And also our Game:

 @override
KeyEventResult onKeyEvent(
RawKeyEvent event,
Set<LogicalKeyboardKey> keysPressed,
) {
if (event is RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowLeft) {
square.state = MovingState.left;
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight) {
square.state = MovingState.right;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
square.state = MovingState.up;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
square.state = MovingState.down;
}
} else {
square.state = MovingState.still;
}
return KeyEventResult.handled;
}
}

With the addition of the MovingState enumeration, we updated the code accordingly to make the movement of our square smooth and introduced state management. We utilized the dt variable from the update function to calculate the movement, resulting in smoother gameplay.

It looks way better now. Now it’s time to add some other components to make our game playable.

Collision handling

To enable collisions in our game, we need to add more components that can collide. Before we proceed, let’s understand the theory behind collisions in Flame.

To make a component collidable, we’ll add the CollisionCallbacks mixin, which provides useful collision-related callbacks. Additionally, we'll introduce the concept of hitboxes. A hitbox represents the area or shape that can collide with other components. We can have multiple hitboxes within a single component, allowing us to accurately calculate collisions.

Well enough words let’s create such component.

class ObstacleParentComponent extends PositionComponent
with CollisionCallbacks {
static const double speed = 80; // Adjust the speed as per your preference
static const double obstacleWidth =
50; // Adjust the width as per your preference
static const double obstacleHeight = 300;
late RectangleHitbox hitbox;

ObstacleParentComponent(double x, double y) {
position = Vector2(x, y);
size = Vector2(obstacleWidth, obstacleHeight);
}

@override
void onLoad() {
super.onLoad();

final defaultPaint = Paint()
..color = Colors.orangeAccent
..style = PaintingStyle.fill;
hitbox = RectangleHitbox()
..paint = defaultPaint
..renderShape = true;

add(hitbox);
}

@override
void update(double dt) {
super.update(dt);

final movementDistance = speed * dt;
position.x += movementDistance;
}
}

In the updated code, we introduced the ObstacleParentComponent and modified the SquareGameComponent to utilize hitboxes and become collidable. With these additions, we can now handle collisions between different components.

To demonstrate collision handling, we generate obstacles randomly and move the SquareGameComponent when a collision occurs.

class SquareGameComponent extends PositionComponent with CollisionCallbacks, 
HasGameReference<SquareGame> {
static const double speed = 20;
static const double movementSpeed = 180;
MovingState state = MovingState.still;
late RectangleHitbox hitbox;

SquareGameComponent(this.game);

@override
void onLoad() {
final defaultPaint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
hitbox = RectangleHitbox()
..paint = defaultPaint
..renderShape = true;
add(hitbox);
}

@override
void update(double dt) {
super.update(dt);

position.x += speed * dt;

switch (state) {
case MovingState.left:
moveLeft(dt);
break;
case MovingState.right:
moveRight(dt);
break;
case MovingState.up:
moveUp(dt);
break;
case MovingState.down:
moveDown(dt);
break;
case MovingState.still:
break;
}
}

void moveLeft(double dt) {
position.x -= movementSpeed * dt;
}

void moveRight(double dt) {
position.x += movementSpeed * dt;
}

void moveUp(double dt) {
position.y -= movementSpeed * dt;
}

void moveDown(double dt) {
position.y += movementSpeed * dt;
}
}

I replaced simple Rect with RectangleHitbox, and set the same size as it was before. And finally the most important part of collision handling will happen in the parent component which in our case is SquareGame.

First part here is obstacle generation. I’ve chosen simplest way: endless two seconds timer, which adds new component in each iteration inside onLoad function

obstacleTimer = Stream.periodic(const Duration(seconds: 2), (timer) {
final obstacleY = Random().nextDouble() *
(size.y - ObstacleParentComponent.obstacleHeight);
final obstacle = ObstacleParentComponent(0, obstacleY);
add(obstacle);
});
obstacleTimer.listen((_) {});

And here is the logic for movement during components colision

 @override
void update(double dt) {
super.update(dt);
moveSquareWithObstacles(dt);
}

void moveSquareWithObstacles(double dt) {
final obstacles = children.whereType<ObstacleParentComponent>();

for (final obstacle in obstacles) {
if (square.collidingWith(obstacle)) {
final squareCenter = square.position;
final obstacleCenter = obstacle.position;
final distanceX = (squareCenter.x - obstacleCenter.x).abs -
(square.width/2 + obstacle.width/2)
print(distanceX);

square.position.x += distanceX * dt;
}
}
}

Let’s check some useful functions and variables here

  • children — through this getter you have access to all children components inside parent component.
  • collidingWith — function which returns boolean and shows that two components are colliding.

Finally we should not forget to add HasCollisionDetection mixin to our game. Well let’s check the result.

Well it already looks like a game :)

Jumping back to the collision callbacks. After we add CollisionCallbacks mixin, we can override some functions

  • onCollisionStart — executes when two components are starting to collide
  • onCollisionEnd — executes when collision is over
  • onCollision — similar to update function, executes every dt of collision.

You can use this callbacks for all imaginable gameplay logic e.g score, lives whatever you want

With these changes, we’ve successfully enhanced our game with smooth movement, state management, and collision handling.

Congratulations on your progress so far! Stay tuned for the next part, where we’ll explore more exciting features and continue building upon our game.

Happy coding!

--

--