Enemies and Bullets collision¶
Right, we are really close to a playable game, we have enemies and we have the ability to shoot bullets at them! We now need to do something when a bullet hits an enemy.
Flame provides a collision detection system out of the box, which we will use to implement our logic when a bullet and an enemy come into contact. The result will be that both are removed!
First we need to let our FlameGame
know that we want collisions between components to
be checked. In order to do so, simply add the HasCollisionDetection
mixin to the declaration
of the game class:
class SpaceShooterGame extends FlameGame
with PanDetector, HasCollisionDetection {
// ...
}
With that, Flame now will start to check if components have collided with each other. Next we need to identify which components can cause collisions.
In our case those are the Bullet
and Enemy
components and we need to add hitboxes to them.
A hitbox is nothing more than a defined part of the component’s area that can hit
other objects. Flame offers a collection of classes to define a hitbox, the simplest of them is
the RectangleHitbox
, which like the name implies, will set a rectangular area as the component’s
hitbox.
Hitboxes are also components, so we can simply add them to the components that we want to have hitboxes.
Let’s start by adding the following line to the Enemy
class:
add(RectangleHitbox());
For the bullet we will do the same, but with a slight difference:
add(
RectangleHitbox(
collisionType: CollisionType.passive,
),
);
The collisionType
s are very important to understand, since they can directly impact the game
performance!
There are three types of collisions in Flame:
active
collides with other hitboxes of type active or passivepassive
collides with other hitboxes of type activeinactive
will not collide with any other hitbox
Usually it is smart to mark hitboxes
from components that will have a higher number of instances
as passive, so they will be taken into account for collision, but they themselves will not check
their own collisions, drastically reducing the number of checking, giving a better performance
to the game!
And since in this game we anticipate that there will be more bullets than enemies, we set the bullets to have a passive collision type!
From this point on, Flame will take care of checking the collision between those two components and we now need to do something when this occurs.
We start by receiving the collision events in one of the classes. Since Bullet
s have a
passive collision type, we will also add the collision checking logic to the Enemy
class.
To listen for collision events we need to add the CollisionCallbacks
mixin to the component.
By doing so we will be able to override some methods like onCollisionStart()
and onCollisionEnd()
.
So let’s make a few changes to the Enemy
class:
class Enemy extends SpriteAnimationComponent
with HasGameReference<SpaceShooterGame>, CollisionCallbacks {
// Other methods omitted
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is Bullet) {
removeFromParent();
other.removeFromParent();
}
}
}
As you can see, we added the mixin to the class, overrode the onCollisionStart
method,
where we check whether the component that collided with us was a Bullet
and if it was, then
we remove both the current Enemy
instance and the Bullet
.
If you run the game now you will finally be able to defeat the enemies crawling down the screen!
To add some final touches, let’s add some explosion animations to introduce more action to the game!
First, let’s create the explosion class:
class Explosion extends SpriteAnimationComponent
with HasGameReference<SpaceShooterGame> {
Explosion({
super.position,
}) : super(
size: Vector2.all(150),
anchor: Anchor.center,
removeOnFinish: true,
);
@override
Future<void> onLoad() async {
await super.onLoad();
animation = await game.loadSpriteAnimation(
'explosion.png',
SpriteAnimationData.sequenced(
amount: 6,
stepTime: .1,
textureSize: Vector2.all(32),
loop: false,
),
);
}
}
There is not much new in it, the biggest difference compared to the other animation components is
that we are passing loop: false
in the SpriteAnimationData.sequenced
constructor and that we are
setting removeOnFinish: true;
. We do that so that when the animation is finished, it will
automatically be removed from the game!
And finally, we make a small change in the onCollisionStart()
method in the Enemy
class
in order to add the explosion to the game:
@override
void onCollisionStart(
Set<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
if (other is Bullet) {
removeFromParent();
other.removeFromParent();
game.add(Explosion(position: position));
}
}
And that is it! We finally have a game which provides all the minimum necessary elements for a space shooter, from here you can use what you learned to build more features in the game like making the player suffer damage if it clashes with an enemy, or make the enemies shoot back, or maybe both?
Good hunting pilot, and happy coding!
1import 'package:flame/collisions.dart';
2import 'package:flame/components.dart';
3import 'package:flame/events.dart';
4import 'package:flame/experimental.dart';
5import 'package:flame/game.dart';
6import 'package:flame/input.dart';
7import 'package:flame/parallax.dart';
8import 'package:flutter/material.dart';
9
10void main() {
11 runApp(GameWidget(game: SpaceShooterGame()));
12}
13
14class SpaceShooterGame extends FlameGame
15 with PanDetector, HasCollisionDetection {
16 late Player player;
17
18 @override
19 Future<void> onLoad() async {
20 final parallax = await loadParallaxComponent(
21 [
22 ParallaxImageData('stars_0.png'),
23 ParallaxImageData('stars_1.png'),
24 ParallaxImageData('stars_2.png'),
25 ],
26 baseVelocity: Vector2(0, -5),
27 repeat: ImageRepeat.repeat,
28 velocityMultiplierDelta: Vector2(0, 5),
29 );
30 add(parallax);
31
32 player = Player();
33 add(player);
34
35 add(
36 SpawnComponent(
37 factory: (index) {
38 return Enemy();
39 },
40 period: 1,
41 area: Rectangle.fromLTWH(0, 0, size.x, -Enemy.enemySize),
42 ),
43 );
44 }
45
46 @override
47 void onPanUpdate(DragUpdateInfo info) {
48 player.move(info.delta.global);
49 }
50
51 @override
52 void onPanStart(DragStartInfo info) {
53 player.startShooting();
54 }
55
56 @override
57 void onPanEnd(DragEndInfo info) {
58 player.stopShooting();
59 }
60}
61
62class Player extends SpriteAnimationComponent
63 with HasGameReference<SpaceShooterGame> {
64 Player()
65 : super(
66 size: Vector2(100, 150),
67 anchor: Anchor.center,
68 );
69
70 late final SpawnComponent _bulletSpawner;
71
72 @override
73 Future<void> onLoad() async {
74 await super.onLoad();
75
76 animation = await game.loadSpriteAnimation(
77 'player.png',
78 SpriteAnimationData.sequenced(
79 amount: 4,
80 stepTime: 0.2,
81 textureSize: Vector2(32, 48),
82 ),
83 );
84
85 position = game.size / 2;
86
87 _bulletSpawner = SpawnComponent(
88 period: 0.2,
89 selfPositioning: true,
90 factory: (index) {
91 return Bullet(
92 position:
93 position +
94 Vector2(
95 0,
96 -height / 2,
97 ),
98 );
99 },
100 autoStart: false,
101 );
102
103 game.add(_bulletSpawner);
104 }
105
106 void move(Vector2 delta) {
107 position.add(delta);
108 }
109
110 void startShooting() {
111 _bulletSpawner.timer.start();
112 }
113
114 void stopShooting() {
115 _bulletSpawner.timer.stop();
116 }
117}
118
119class Bullet extends SpriteAnimationComponent
120 with HasGameReference<SpaceShooterGame> {
121 Bullet({
122 super.position,
123 }) : super(
124 size: Vector2(25, 50),
125 anchor: Anchor.center,
126 );
127
128 @override
129 Future<void> onLoad() async {
130 await super.onLoad();
131
132 animation = await game.loadSpriteAnimation(
133 'bullet.png',
134 SpriteAnimationData.sequenced(
135 amount: 4,
136 stepTime: 0.2,
137 textureSize: Vector2(8, 16),
138 ),
139 );
140
141 add(
142 RectangleHitbox(
143 collisionType: CollisionType.passive,
144 ),
145 );
146 }
147
148 @override
149 void update(double dt) {
150 super.update(dt);
151
152 position.y += dt * -500;
153
154 if (position.y < -height) {
155 removeFromParent();
156 }
157 }
158}
159
160class Enemy extends SpriteAnimationComponent
161 with HasGameReference<SpaceShooterGame>, CollisionCallbacks {
162 Enemy({
163 super.position,
164 }) : super(
165 size: Vector2.all(enemySize),
166 anchor: Anchor.center,
167 );
168
169 static const enemySize = 50.0;
170
171 @override
172 Future<void> onLoad() async {
173 await super.onLoad();
174
175 animation = await game.loadSpriteAnimation(
176 'enemy.png',
177 SpriteAnimationData.sequenced(
178 amount: 4,
179 stepTime: 0.2,
180 textureSize: Vector2.all(16),
181 ),
182 );
183
184 add(RectangleHitbox());
185 }
186
187 @override
188 void update(double dt) {
189 super.update(dt);
190
191 position.y += dt * 250;
192
193 if (position.y > game.size.y) {
194 removeFromParent();
195 }
196 }
197
198 @override
199 void onCollisionStart(
200 Set<Vector2> intersectionPoints,
201 PositionComponent other,
202 ) {
203 super.onCollisionStart(intersectionPoints, other);
204
205 if (other is Bullet) {
206 removeFromParent();
207 other.removeFromParent();
208 game.add(Explosion(position: position));
209 }
210 }
211}
212
213class Explosion extends SpriteAnimationComponent
214 with HasGameReference<SpaceShooterGame> {
215 Explosion({
216 super.position,
217 }) : super(
218 size: Vector2.all(150),
219 anchor: Anchor.center,
220 removeOnFinish: true,
221 );
222
223 @override
224 Future<void> onLoad() async {
225 await super.onLoad();
226
227 animation = await game.loadSpriteAnimation(
228 'explosion.png',
229 SpriteAnimationData.sequenced(
230 amount: 6,
231 stepTime: 0.1,
232 textureSize: Vector2.all(32),
233 loop: false,
234 ),
235 );
236 }
237}