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 ofEnemy
.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!
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}