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. We need a bullet sprite for it. Right-click the image below, choose “Save as…”, and store it as bullet.png in your assets/images/ folder:

bullet

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

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

    animation = await gameRef.loadSpriteAnimation(
      'assets/images/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 HasGameRef<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 HasGameRef<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 onDragStart() and onDragEnd() methods from the DragCallbacks mixin that we already have been using for the ship movement:

class SpaceShooterGame extends FlameGame with DragCallbacks {
  late Player player;

  // Rest of implementation omitted

  @override
  void onDragUpdate(DragUpdateEvent event) {
    player.move(event.localDelta);
  }

  @override
  void onDragStart(DragStartEvent event) {
    super.onDragStart(event);
    player.startShooting();
  }

  @override
  void onDragEnd(DragEndEvent event) {
    super.onDragEnd(event);
    player.stopShooting();
  }
}

Note that onDragStart and onDragEnd keep track of the drag state for us, so our overrides have to call super before doing their own work.

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 HasGameRef<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));
      },
      autoStart: false,
    );

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

Next step: Adding Enemies