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

enemy

class Enemy extends SpriteAnimationComponent with HasGameRef<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 gameRef.loadSpriteAnimation(
      'assets/images/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 > gameRef.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/parallax.dart';
  6import 'package:flutter/material.dart';
  7
  8void main() {
  9  runApp(GameWidget(game: SpaceShooterGame()));
 10}
 11
 12class SpaceShooterGame extends FlameGame with DragCallbacks {
 13  late Player player;
 14
 15  @override
 16  Future<void> onLoad() async {
 17    final parallax = await loadParallaxComponent(
 18      [
 19        ParallaxImageData('assets/images/stars_0.png'),
 20        ParallaxImageData('assets/images/stars_1.png'),
 21        ParallaxImageData('assets/images/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    add(
 33      SpawnComponent(
 34        factory: (index) {
 35          return Enemy();
 36        },
 37        period: 1,
 38        area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),
 39      ),
 40    );
 41  }
 42
 43  @override
 44  void onDragUpdate(DragUpdateEvent event) {
 45    player.move(event.localDelta);
 46  }
 47
 48  @override
 49  void onDragStart(DragStartEvent event) {
 50    super.onDragStart(event);
 51    player.startShooting();
 52  }
 53
 54  @override
 55  void onDragEnd(DragEndEvent event) {
 56    super.onDragEnd(event);
 57    player.stopShooting();
 58  }
 59}
 60
 61class Player extends SpriteAnimationComponent
 62    with HasGameRef<SpaceShooterGame> {
 63  Player()
 64    : super(
 65        size: Vector2(100, 150),
 66        anchor: Anchor.center,
 67      );
 68
 69  late final SpawnComponent _bulletSpawner;
 70
 71  @override
 72  Future<void> onLoad() async {
 73    await super.onLoad();
 74
 75    animation = await gameRef.loadSpriteAnimation(
 76      'assets/images/player.png',
 77      SpriteAnimationData.sequenced(
 78        amount: 4,
 79        stepTime: 0.2,
 80        textureSize: Vector2(32, 48),
 81      ),
 82    );
 83
 84    position = gameRef.size / 2;
 85
 86    _bulletSpawner = SpawnComponent(
 87      period: 0.2,
 88      selfPositioning: true,
 89      factory: (index) {
 90        return Bullet(
 91          position:
 92              position +
 93              Vector2(
 94                0,
 95                -height / 2,
 96              ),
 97        );
 98      },
 99      autoStart: false,
100    );
101
102    gameRef.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 HasGameRef<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 gameRef.loadSpriteAnimation(
132      'assets/images/bullet.png',
133      SpriteAnimationData.sequenced(
134        amount: 4,
135        stepTime: 0.2,
136        textureSize: Vector2(8, 16),
137      ),
138    );
139  }
140
141  @override
142  void update(double dt) {
143    super.update(dt);
144
145    position.y += dt * -500;
146
147    if (position.y < -height) {
148      removeFromParent();
149    }
150  }
151}
152
153class Enemy extends SpriteAnimationComponent with HasGameRef<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 gameRef.loadSpriteAnimation(
168      'assets/images/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 > gameRef.size.y) {
184      removeFromParent();
185    }
186  }
187}

Next step: Collision Detection