Adding bullets

For this next step we will add a very important feature to any space shooter game, shooting!

Here is how we will implement it: since we already control our space ship by dragging on the screen with the mouse/fingers, we will make the ship auto shoot when the player starts dragging and stop shooting when the gesture/input has ended.

First, let’s create a Bullet component that will represent the shots in the game.

class Bullet extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {
  Bullet({
    super.position,
  }) : super(
          size: Vector2(25, 50),
          anchor: Anchor.center,
        );

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

    animation = await game.loadSpriteAnimation(
      'bullet.png',
      SpriteAnimationData.sequenced(
        amount: 4,
        stepTime: .2,
        textureSize: Vector2(8, 16),
      ),
    );
  }
}

So far, this does not introduce any new concepts, we just created a component and set up its animations attributes.

The Bullet behavior is a simple one, it always moves towards the top of the screen and should be removed from the game if it is not visible anymore, so let’s add an update method to it and make it happen:

class Bullet extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {
  Bullet({
    super.position,
  }) : super(
          size: Vector2(25, 50),
          anchor: Anchor.center,
        );

  @override
  Future<void> onLoad() async {
    // Omitted
  }

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

    position.y += dt * -500;

    if (position.y < -height) {
      removeFromParent();
    }
  }
}

The above code should be straight forward, but lets break it down:

  • We add to the bullet’s y axis position at a rate of -500 pixels per second. Remember going up in the y axis means getting closer to 0 since the top left corner of the screen is 0, 0.

  • If the y is smaller than the negative value of the bullet’s height, means that the component is completely off the screen and it can be removed.

Right, we now have a Bullet class ready, so lets start to implement the action of shooting. First thing, let’s create two empty methods in the Player class, startShooting() and stopShooting().

class Player extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {

  // Rest of implementation omitted

  void startShooting() {
    // TODO
  }

  void stopShooting() {
    // TODO
  }
}

And let’s hook into those methods from the game class by using the onPanStart() and onPanEnd() methods from the PanDetector mixin that we already have been using for the ship movement:

class SpaceShooterGame extends FlameGame with PanDetector {
  late Player player;

  // Rest of implementation omitted

  @override
  void onPanUpdate(DragUpdateInfo info) {
    player.move(info.delta.global);
  }

  @override
  void onPanStart(DragStartInfo info) {
    player.startShooting();
  }

  @override
  void onPanEnd(DragEndInfo info) {
    player.stopShooting();
  }
}

We now have everything set up, so let’s write the shooting routine in our player class.

Remember, the shooting behavior will be adding bullets through time intervals when the player is dragging the starship.

We could implement the time interval code and the spawning manually, but Flame provides a component out of the box for that, the SpawnComponent, so let’s take advantage of it:

class Player extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {
  late final SpawnComponent _bulletSpawner;

  @override
  Future<void> onLoad() async {
    // Loading animation omitted

    _bulletSpawner = SpawnComponent(
      period: .2,
      selfPositioning: true,
      factory: (index) {
        return Bullet(
          position: position +
              Vector2(
                0,
                -height / 2,
              ),
        );

        return bullet;
      },
      autoStart: false,
    );

    game.add(_bulletSpawner);
  }

  void move(Vector2 delta) {
    position.add(delta);
  }

  void startShooting() {
    _bulletSpawner.timer.start();
  }

  void stopShooting() {
    _bulletSpawner.timer.stop();
  }
}

Hopefully the code above speaks for itself, but let’s look at it in more detail:

  • First we declared a SpawnComponent called _bulletSpawner in our game class, we needed it to be an variable accessible to the whole component since we will be accessing it in the startShooting and stopShooting methods.

  • We initialize our _bulletSpawner in the onLoad method. In the first argument, period, we set how much time in seconds it will take between calls, and we choose .2 seconds for now.

  • We set selfPositioning: true so the spawn component doesn’t try to position the created component since we want to handle that ourselves to make the bullets spawn out of the ship.

  • The factory attribute receives a function that will be called every time the period is
    reached and return the created component.

  • We set autoStart: false so it does not start by default.

  • Finally we add the _bulletSpawner to our component, so it can be processed in the game loop.

  • Note how the _bulletSpawner is added to the game instead of the player, since the bullets are part of the whole game and not the player itself.

With the _bulletSpawner all set up, the only missing piece now is starting the _bulletSpawner.timer in startShooting() and stopping it in the stopShooting()!

And that closes this step, putting us real close to a real game!

main.dart
  1import 'package:flame/components.dart';
  2import 'package:flame/events.dart';
  3import 'package:flame/game.dart';
  4import 'package:flame/input.dart';
  5import 'package:flame/parallax.dart';
  6import 'package:flutter/material.dart';
  7
  8void main() {
  9  runApp(GameWidget(game: SpaceShooterGame()));
 10}
 11
 12class SpaceShooterGame extends FlameGame with PanDetector {
 13  late Player player;
 14
 15  @override
 16  Future<void> onLoad() async {
 17    final parallax = await loadParallaxComponent(
 18      [
 19        ParallaxImageData('stars_0.png'),
 20        ParallaxImageData('stars_1.png'),
 21        ParallaxImageData('stars_2.png'),
 22      ],
 23      baseVelocity: Vector2(0, -5),
 24      repeat: ImageRepeat.repeat,
 25      velocityMultiplierDelta: Vector2(0, 5),
 26    );
 27    add(parallax);
 28
 29    player = Player();
 30    add(player);
 31  }
 32
 33  @override
 34  void onPanUpdate(DragUpdateInfo info) {
 35    player.move(info.delta.global);
 36  }
 37
 38  @override
 39  void onPanStart(DragStartInfo info) {
 40    player.startShooting();
 41  }
 42
 43  @override
 44  void onPanEnd(DragEndInfo info) {
 45    player.stopShooting();
 46  }
 47}
 48
 49class Player extends SpriteAnimationComponent
 50    with HasGameReference<SpaceShooterGame> {
 51  Player()
 52      : super(
 53          size: Vector2(100, 150),
 54          anchor: Anchor.center,
 55        );
 56
 57  late final SpawnComponent _bulletSpawner;
 58
 59  @override
 60  Future<void> onLoad() async {
 61    await super.onLoad();
 62
 63    animation = await game.loadSpriteAnimation(
 64      'player.png',
 65      SpriteAnimationData.sequenced(
 66        amount: 4,
 67        stepTime: 0.2,
 68        textureSize: Vector2(32, 48),
 69      ),
 70    );
 71
 72    position = game.size / 2;
 73
 74    _bulletSpawner = SpawnComponent(
 75      period: 0.2,
 76      selfPositioning: true,
 77      factory: (index) {
 78        return Bullet(
 79          position: position +
 80              Vector2(
 81                0,
 82                -height / 2,
 83              ),
 84        );
 85      },
 86      autoStart: false,
 87    );
 88
 89    game.add(_bulletSpawner);
 90  }
 91
 92  void move(Vector2 delta) {
 93    position.add(delta);
 94  }
 95
 96  void startShooting() {
 97    _bulletSpawner.timer.start();
 98  }
 99
100  void stopShooting() {
101    _bulletSpawner.timer.stop();
102  }
103}
104
105class Bullet extends SpriteAnimationComponent
106    with HasGameReference<SpaceShooterGame> {
107  Bullet({
108    super.position,
109  }) : super(
110          size: Vector2(25, 50),
111          anchor: Anchor.center,
112        );
113
114  @override
115  Future<void> onLoad() async {
116    await super.onLoad();
117
118    animation = await game.loadSpriteAnimation(
119      'bullet.png',
120      SpriteAnimationData.sequenced(
121        amount: 4,
122        stepTime: 0.2,
123        textureSize: Vector2(8, 16),
124      ),
125    );
126  }
127
128  @override
129  void update(double dt) {
130    super.update(dt);
131
132    position.y += dt * -500;
133
134    if (position.y < -height) {
135      removeFromParent();
136    }
137  }
138}

Next step: Adding Enemies