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: position +
 91              Vector2(
 92                0,
 93                -height / 2,
 94              ),
 95        );
 96      },
 97      autoStart: false,
 98    );
 99
100    game.add(_bulletSpawner);
101  }
102
103  void move(Vector2 delta) {
104    position.add(delta);
105  }
106
107  void startShooting() {
108    _bulletSpawner.timer.start();
109  }
110
111  void stopShooting() {
112    _bulletSpawner.timer.stop();
113  }
114}
115
116class Bullet extends SpriteAnimationComponent
117    with HasGameReference<SpaceShooterGame> {
118  Bullet({
119    super.position,
120  }) : super(
121          size: Vector2(25, 50),
122          anchor: Anchor.center,
123        );
124
125  @override
126  Future<void> onLoad() async {
127    await super.onLoad();
128
129    animation = await game.loadSpriteAnimation(
130      'bullet.png',
131      SpriteAnimationData.sequenced(
132        amount: 4,
133        stepTime: 0.2,
134        textureSize: Vector2(8, 16),
135      ),
136    );
137  }
138
139  @override
140  void update(double dt) {
141    super.update(dt);
142
143    position.y += dt * -500;
144
145    if (position.y < -height) {
146      removeFromParent();
147    }
148  }
149}
150
151class Enemy extends SpriteAnimationComponent
152    with HasGameReference<SpaceShooterGame> {
153  Enemy({
154    super.position,
155  }) : super(
156          size: Vector2.all(enemySize),
157          anchor: Anchor.center,
158        );
159
160  static const enemySize = 50.0;
161
162  @override
163  Future<void> onLoad() async {
164    await super.onLoad();
165
166    animation = await game.loadSpriteAnimation(
167      'enemy.png',
168      SpriteAnimationData.sequenced(
169        amount: 4,
170        stepTime: 0.2,
171        textureSize: Vector2.all(16),
172      ),
173    );
174  }
175
176  @override
177  void update(double dt) {
178    super.update(dt);
179
180    position.y += dt * 250;
181
182    if (position.y > game.size.y) {
183      removeFromParent();
184    }
185  }
186}

Next step: Collision Detection