Adding Enemies

Now that the starship is able to shoot, we need something for the player to shoot at! So for this step we will work on adding enemies to the game.

First, let’s create an Enemy class that will represent the enemies in game:

class Enemy extends SpriteAnimationComponent
    with HasGameReference<SpaceShooterGame> {

  Enemy({
    super.position,
  }) : super(
          size: Vector2.all(enemySize),
          anchor: Anchor.center,
        );


  static const enemySize = 50.0;

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

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

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

    position.y += dt * 250;

    if (position.y > game.size.y) {
      removeFromParent();
    }
  }
}

Note that for now, the Enemy class is super similar to the Bullet one, the only differences are their sizes, animation information and that bullets travel from bottom to top, while enemies travel from top to bottom, so nothing new here.

Next we need to make the enemies spawn in the game, the logic here will be simple: we will make enemies spawn from the top of the screen at a random position on the x axis.

Once again, we could manually add all the time based events in the game’s update() method, maintain a random instance to get the enemy x position and so on and so forth, but Flame provides us with a way to avoid having to write all that by ourselves: we can use the SpawnComponent! So in the SpaceShooterGame.onLoad() method let’s add the following code:

    add(
      SpawnComponent(
        factory: (index) {
          return Enemy();
        },
        period: 1,
        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),
      ),
    );

The SpawnComponent will take a couple of arguments, let’s review them as they appear in the code:

  • factory receives a function which has the index of the component that should be created. We don’t use the index in our code, but it is useful to create more advanced spawn routines. This function should return the created component, in our case a new instance of Enemy.

  • period simply define the interval in which a new component will be spawned.

  • area defines the possible area where the components can be placed once created. In our case they should be placed in the area above the screen top, so they can be seen as they are arriving into the playable area.

And this concludes this short step!

main.dart
  1import 'package:flame/components.dart';
  2import 'package:flame/events.dart';
  3import 'package:flame/experimental.dart';
  4import 'package:flame/game.dart';
  5import 'package:flame/input.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 with PanDetector {
 14  late Player player;
 15
 16  @override
 17  Future<void> onLoad() async {
 18    final parallax = await loadParallaxComponent(
 19      [
 20        ParallaxImageData('stars_0.png'),
 21        ParallaxImageData('stars_1.png'),
 22        ParallaxImageData('stars_2.png'),
 23      ],
 24      baseVelocity: Vector2(0, -5),
 25      repeat: ImageRepeat.repeat,
 26      velocityMultiplierDelta: Vector2(0, 5),
 27    );
 28    add(parallax);
 29
 30    player = Player();
 31    add(player);
 32
 33    add(
 34      SpawnComponent(
 35        factory: (index) {
 36          return Enemy();
 37        },
 38        period: 1,
 39        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),
 40      ),
 41    );
 42  }
 43
 44  @override
 45  void onPanUpdate(DragUpdateInfo info) {
 46    player.move(info.delta.global);
 47  }
 48
 49  @override
 50  void onPanStart(DragStartInfo info) {
 51    player.startShooting();
 52  }
 53
 54  @override
 55  void onPanEnd(DragEndInfo info) {
 56    player.stopShooting();
 57  }
 58}
 59
 60class Player extends SpriteAnimationComponent
 61    with HasGameReference<SpaceShooterGame> {
 62  Player()
 63    : super(
 64        size: Vector2(100, 150),
 65        anchor: Anchor.center,
 66      );
 67
 68  late final SpawnComponent _bulletSpawner;
 69
 70  @override
 71  Future<void> onLoad() async {
 72    await super.onLoad();
 73
 74    animation = await game.loadSpriteAnimation(
 75      'player.png',
 76      SpriteAnimationData.sequenced(
 77        amount: 4,
 78        stepTime: 0.2,
 79        textureSize: Vector2(32, 48),
 80      ),
 81    );
 82
 83    position = game.size / 2;
 84
 85    _bulletSpawner = SpawnComponent(
 86      period: 0.2,
 87      selfPositioning: true,
 88      factory: (index) {
 89        return Bullet(
 90          position:
 91              position +
 92              Vector2(
 93                0,
 94                -height / 2,
 95              ),
 96        );
 97      },
 98      autoStart: false,
 99    );
100
101    game.add(_bulletSpawner);
102  }
103
104  void move(Vector2 delta) {
105    position.add(delta);
106  }
107
108  void startShooting() {
109    _bulletSpawner.timer.start();
110  }
111
112  void stopShooting() {
113    _bulletSpawner.timer.stop();
114  }
115}
116
117class Bullet extends SpriteAnimationComponent
118    with HasGameReference<SpaceShooterGame> {
119  Bullet({
120    super.position,
121  }) : super(
122         size: Vector2(25, 50),
123         anchor: Anchor.center,
124       );
125
126  @override
127  Future<void> onLoad() async {
128    await super.onLoad();
129
130    animation = await game.loadSpriteAnimation(
131      'bullet.png',
132      SpriteAnimationData.sequenced(
133        amount: 4,
134        stepTime: 0.2,
135        textureSize: Vector2(8, 16),
136      ),
137    );
138  }
139
140  @override
141  void update(double dt) {
142    super.update(dt);
143
144    position.y += dt * -500;
145
146    if (position.y < -height) {
147      removeFromParent();
148    }
149  }
150}
151
152class Enemy extends SpriteAnimationComponent
153    with HasGameReference<SpaceShooterGame> {
154  Enemy({
155    super.position,
156  }) : super(
157         size: Vector2.all(enemySize),
158         anchor: Anchor.center,
159       );
160
161  static const enemySize = 50.0;
162
163  @override
164  Future<void> onLoad() async {
165    await super.onLoad();
166
167    animation = await game.loadSpriteAnimation(
168      'enemy.png',
169      SpriteAnimationData.sequenced(
170        amount: 4,
171        stepTime: 0.2,
172        textureSize: Vector2.all(16),
173      ),
174    );
175  }
176
177  @override
178  void update(double dt) {
179    super.update(dt);
180
181    position.y += dt * 250;
182
183    if (position.y > game.size.y) {
184      removeFromParent();
185    }
186  }
187}

Next step: Collision Detection