Enemies and Bullets collision

Right, we are really close to a playable game, we have enemies and we have the ability to shoot bullets at them! We now need to do something when a bullet hits an enemy.

Flame provides a collision detection system out of the box, which we will use to implement our logic when a bullet and an enemy come into contact. The result will be that both are removed!

First we need to let our FlameGame know that we want collisions between components to be checked. In order to do so, simply add the HasCollisionDetection mixin to the declaration of the game class:

class SpaceShooterGame extends FlameGame
    with PanDetector, HasCollisionDetection {
    // ...
}

With that, Flame now will start to check if components have collided with each other. Next we need to identify which components can cause collisions.

In our case those are the Bullet and Enemy components and we need to add hitboxes to them.

A hitbox is nothing more than a defined part of the component’s area that can hit other objects. Flame offers a collection of classes to define a hitbox, the simplest of them is the RectangleHitbox, which like the name implies, will set a rectangular area as the component’s hitbox.

Hitboxes are also components, so we can simply add them to the components that we want to have hitboxes. Let’s start by adding the following line to the Enemy class:

add(RectangleHitbox());

For the bullet we will do the same, but with a slight difference:

add(
  RectangleHitbox(
    collisionType: CollisionType.passive,
  ),
);

The collisionTypes are very important to understand, since they can directly impact the game performance!

There are three types of collisions in Flame:

  • active collides with other hitboxes of type active or passive

  • passive collides with other hitboxes of type active

  • inactive will not collide with any other hitbox

Usually it is smart to mark hitboxes from components that will have a higher number of instances as passive, so they will be taken into account for collision, but they themselves will not check their own collisions, drastically reducing the number of checking, giving a better performance to the game!

And since in this game we anticipate that there will be more bullets than enemies, we set the bullets to have a passive collision type!

From this point on, Flame will take care of checking the collision between those two components and we now need to do something when this occurs.

We start by receiving the collision events in one of the classes. Since Bullets have a passive collision type, we will also add the collision checking logic to the Enemy class.

To listen for collision events we need to add the CollisionCallbacks mixin to the component. By doing so we will be able to override some methods like onCollisionStart() and onCollisionEnd().

So let’s make a few changes to the Enemy class:

class Enemy extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {

  // Other methods omitted

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);

    if (other is Bullet) {
      removeFromParent();
      other.removeFromParent();
    }
  }
}

As you can see, we added the mixin to the class, overrode the onCollisionStart method, where we check whether the component that collided with us was a Bullet and if it was, then we remove both the current Enemy instance and the Bullet.

If you run the game now you will finally be able to defeat the enemies crawling down the screen!

To add some final touches, let’s add some explosion animations to introduce more action to the game!

First, let’s create the explosion class:

class Explosion extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {
  Explosion({
    super.position,
  }) : super(
          size: Vector2.all(150),
          anchor: Anchor.center,
          removeOnFinish: true,
        );


  @override
  Future<void> onLoad() async {
    await super.onLoad();

    animation = await game.loadSpriteAnimation(
      'explosion.png',
      SpriteAnimationData.sequenced(
        amount: 6,
        stepTime: .1,
        textureSize: Vector2.all(32),
        loop: false,
      ),
    );
  }
}

There is not much new in it, the biggest difference compared to the other animation components is that we are passing loop: false in the SpriteAnimationData.sequenced constructor and that we are setting removeOnFinish: true;. We do that so that when the animation is finished, it will automatically be removed from the game!

And finally, we make a small change in the onCollisionStart() method in the Enemy class in order to add the explosion to the game:

  @override
  void onCollisionStart(
    Set<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);

    if (other is Bullet) {
      removeFromParent();
      other.removeFromParent();
      game.add(Explosion(position: position));
    }
  }

And that is it! We finally have a game which provides all the minimum necessary elements for a space shooter, from here you can use what you learned to build more features in the game like making the player suffer damage if it clashes with an enemy, or make the enemies shoot back, or maybe both?

Good hunting pilot, and happy coding!

main.dart
  1import 'package:flame/collisions.dart';
  2import 'package:flame/components.dart';
  3import 'package:flame/events.dart';
  4import 'package:flame/experimental.dart';
  5import 'package:flame/game.dart';
  6import 'package:flame/input.dart';
  7import 'package:flame/parallax.dart';
  8import 'package:flutter/material.dart';
  9
 10void main() {
 11  runApp(GameWidget(game: SpaceShooterGame()));
 12}
 13
 14class SpaceShooterGame extends FlameGame
 15    with PanDetector, HasCollisionDetection {
 16  late Player player;
 17
 18  @override
 19  Future<void> onLoad() async {
 20    final parallax = await loadParallaxComponent(
 21      [
 22        ParallaxImageData('stars_0.png'),
 23        ParallaxImageData('stars_1.png'),
 24        ParallaxImageData('stars_2.png'),
 25      ],
 26      baseVelocity: Vector2(0, -5),
 27      repeat: ImageRepeat.repeat,
 28      velocityMultiplierDelta: Vector2(0, 5),
 29    );
 30    add(parallax);
 31
 32    player = Player();
 33    add(player);
 34
 35    add(
 36      SpawnComponent(
 37        factory: (index) {
 38          return Enemy();
 39        },
 40        period: 1,
 41        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),
 42      ),
 43    );
 44  }
 45
 46  @override
 47  void onPanUpdate(DragUpdateInfo info) {
 48    player.move(info.delta.global);
 49  }
 50
 51  @override
 52  void onPanStart(DragStartInfo info) {
 53    player.startShooting();
 54  }
 55
 56  @override
 57  void onPanEnd(DragEndInfo info) {
 58    player.stopShooting();
 59  }
 60}
 61
 62class Player extends SpriteAnimationComponent
 63    with HasGameReference<SpaceShooterGame> {
 64  Player()
 65    : super(
 66        size: Vector2(100, 150),
 67        anchor: Anchor.center,
 68      );
 69
 70  late final SpawnComponent _bulletSpawner;
 71
 72  @override
 73  Future<void> onLoad() async {
 74    await super.onLoad();
 75
 76    animation = await game.loadSpriteAnimation(
 77      'player.png',
 78      SpriteAnimationData.sequenced(
 79        amount: 4,
 80        stepTime: 0.2,
 81        textureSize: Vector2(32, 48),
 82      ),
 83    );
 84
 85    position = game.size / 2;
 86
 87    _bulletSpawner = SpawnComponent(
 88      period: 0.2,
 89      selfPositioning: true,
 90      factory: (index) {
 91        return Bullet(
 92          position:
 93              position +
 94              Vector2(
 95                0,
 96                -height / 2,
 97              ),
 98        );
 99      },
100      autoStart: false,
101    );
102
103    game.add(_bulletSpawner);
104  }
105
106  void move(Vector2 delta) {
107    position.add(delta);
108  }
109
110  void startShooting() {
111    _bulletSpawner.timer.start();
112  }
113
114  void stopShooting() {
115    _bulletSpawner.timer.stop();
116  }
117}
118
119class Bullet extends SpriteAnimationComponent
120    with HasGameReference<SpaceShooterGame> {
121  Bullet({
122    super.position,
123  }) : super(
124         size: Vector2(25, 50),
125         anchor: Anchor.center,
126       );
127
128  @override
129  Future<void> onLoad() async {
130    await super.onLoad();
131
132    animation = await game.loadSpriteAnimation(
133      'bullet.png',
134      SpriteAnimationData.sequenced(
135        amount: 4,
136        stepTime: 0.2,
137        textureSize: Vector2(8, 16),
138      ),
139    );
140
141    add(
142      RectangleHitbox(
143        collisionType: CollisionType.passive,
144      ),
145    );
146  }
147
148  @override
149  void update(double dt) {
150    super.update(dt);
151
152    position.y += dt * -500;
153
154    if (position.y < -height) {
155      removeFromParent();
156    }
157  }
158}
159
160class Enemy extends SpriteAnimationComponent
161    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {
162  Enemy({
163    super.position,
164  }) : super(
165         size: Vector2.all(enemySize),
166         anchor: Anchor.center,
167       );
168
169  static const enemySize = 50.0;
170
171  @override
172  Future<void> onLoad() async {
173    await super.onLoad();
174
175    animation = await game.loadSpriteAnimation(
176      'enemy.png',
177      SpriteAnimationData.sequenced(
178        amount: 4,
179        stepTime: 0.2,
180        textureSize: Vector2.all(16),
181      ),
182    );
183
184    add(RectangleHitbox());
185  }
186
187  @override
188  void update(double dt) {
189    super.update(dt);
190
191    position.y += dt * 250;
192
193    if (position.y > game.size.y) {
194      removeFromParent();
195    }
196  }
197
198  @override
199  void onCollisionStart(
200    Set<Vector2> intersectionPoints,
201    PositionComponent other,
202  ) {
203    super.onCollisionStart(intersectionPoints, other);
204
205    if (other is Bullet) {
206      removeFromParent();
207      other.removeFromParent();
208      game.add(Explosion(position: position));
209    }
210  }
211}
212
213class Explosion extends SpriteAnimationComponent
214    with HasGameReference<SpaceShooterGame> {
215  Explosion({
216    super.position,
217  }) : super(
218         size: Vector2.all(150),
219         anchor: Anchor.center,
220         removeOnFinish: true,
221       );
222
223  @override
224  Future<void> onLoad() async {
225    await super.onLoad();
226
227    animation = await game.loadSpriteAnimation(
228      'explosion.png',
229      SpriteAnimationData.sequenced(
230        amount: 6,
231        stepTime: 0.1,
232        textureSize: Vector2.all(32),
233        loop: false,
234      ),
235    );
236  }
237}