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 that will handle when a bullet and an enemy comes 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 has 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 make a rectangular area as the component’s hitbox.

Hitboxes are also components, so in order to add them to our components we can simply add them to the components that we want to have hitboxes, so let’s do it, 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 Hitboxes

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 choose the bullets to have a passive collision type!

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

We can start that 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 to 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 do that and 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 and add 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 from 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: .2,
 81        textureSize: Vector2(32, 48),
 82      ),
 83    );
 84
 85    position = game.size / 2;
 86
 87    _bulletSpawner = SpawnComponent(
 88      period: .2,
 89      selfPositioning: true,
 90      factory: (index) {
 91        return Bullet(
 92          position: position +
 93              Vector2(
 94                0,
 95                -height / 2,
 96              ),
 97        );
 98      },
 99      autoStart: false,
100    );
101
102    game.add(_bulletSpawner);
103  }
104
105  void move(Vector2 delta) {
106    position.add(delta);
107  }
108
109  void startShooting() {
110    _bulletSpawner.timer.start();
111  }
112
113  void stopShooting() {
114    _bulletSpawner.timer.stop();
115  }
116}
117
118class Bullet extends SpriteAnimationComponent
119    with HasGameReference<SpaceShooterGame> {
120  Bullet({
121    super.position,
122  }) : super(
123          size: Vector2(25, 50),
124          anchor: Anchor.center,
125        );
126
127  @override
128  Future<void> onLoad() async {
129    await super.onLoad();
130
131    animation = await game.loadSpriteAnimation(
132      'bullet.png',
133      SpriteAnimationData.sequenced(
134        amount: 4,
135        stepTime: .2,
136        textureSize: Vector2(8, 16),
137      ),
138    );
139
140    add(
141      RectangleHitbox(
142        collisionType: CollisionType.passive,
143      ),
144    );
145  }
146
147  @override
148  void update(double dt) {
149    super.update(dt);
150
151    position.y += dt * -500;
152
153    if (position.y < -height) {
154      removeFromParent();
155    }
156  }
157}
158
159class Enemy extends SpriteAnimationComponent
160    with HasGameReference<SpaceShooterGame>, CollisionCallbacks {
161  Enemy({
162    super.position,
163  }) : super(
164          size: Vector2.all(enemySize),
165          anchor: Anchor.center,
166        );
167
168  static const enemySize = 50.0;
169
170  @override
171  Future<void> onLoad() async {
172    await super.onLoad();
173
174    animation = await game.loadSpriteAnimation(
175      'enemy.png',
176      SpriteAnimationData.sequenced(
177        amount: 4,
178        stepTime: .2,
179        textureSize: Vector2.all(16),
180      ),
181    );
182
183    add(RectangleHitbox());
184  }
185
186  @override
187  void update(double dt) {
188    super.update(dt);
189
190    position.y += dt * 250;
191
192    if (position.y > game.size.y) {
193      removeFromParent();
194    }
195  }
196
197  @override
198  void onCollisionStart(
199    Set<Vector2> intersectionPoints,
200    PositionComponent other,
201  ) {
202    super.onCollisionStart(intersectionPoints, other);
203
204    if (other is Bullet) {
205      removeFromParent();
206      other.removeFromParent();
207      game.add(Explosion(position: position));
208    }
209  }
210}
211
212class Explosion extends SpriteAnimationComponent
213    with HasGameReference<SpaceShooterGame> {
214  Explosion({
215    super.position,
216  }) : super(
217          size: Vector2.all(150),
218          anchor: Anchor.center,
219          removeOnFinish: true,
220        );
221
222  @override
223  Future<void> onLoad() async {
224    await super.onLoad();
225
226    animation = await game.loadSpriteAnimation(
227      'explosion.png',
228      SpriteAnimationData.sequenced(
229        amount: 6,
230        stepTime: .1,
231        textureSize: Vector2.all(32),
232        loop: false,
233      ),
234    );
235  }
236}