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 DragCallbacks, 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 HasGameRef<SpaceShooterGame>, CollisionCallbacks {

  // Other methods omitted

  @override
  void onCollisionStart(
    List<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, we need an explosion sprite sheet. Right-click the image below, choose “Save as…”, and store it as explosion.png in your assets/images/ folder:

explosion

Now let’s create the explosion class:

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


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

    animation = await gameRef.loadSpriteAnimation(
      'assets/images/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(
    List<Vector2> intersectionPoints,
    PositionComponent other,
  ) {
    super.onCollisionStart(intersectionPoints, other);

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