Adding bullets¶
For this next step we will add a very important feature to any space shooter game, shooting!
Here is how we will implement it: since we already control our space ship by dragging on the screen with the mouse/fingers, we will make the ship auto shoot when the player starts dragging and stop shooting when the gesture/input has ended.
First, let’s create a Bullet component that will represent the shots in the game. We need a
bullet sprite for it. Right-click the image below, choose “Save as…”, and store it as
bullet.png in your assets/images/ folder:

class Bullet extends SpriteAnimationComponent
with HasGameRef<SpaceShooterGame> {
Bullet({
super.position,
}) : super(
size: Vector2(25, 50),
anchor: Anchor.center,
);
@override
Future<void> onLoad() async {
await super.onLoad();
animation = await gameRef.loadSpriteAnimation(
'assets/images/bullet.png',
SpriteAnimationData.sequenced(
amount: 4,
stepTime: .2,
textureSize: Vector2(8, 16),
),
);
}
}
So far, this does not introduce any new concepts, we just created a component and set up its animations attributes.
The Bullet behavior is a simple one, it always moves towards the top of the screen and should
be removed from the game if it is not visible anymore, so let’s add an update method to it
and make it happen:
class Bullet extends SpriteAnimationComponent
with HasGameRef<SpaceShooterGame> {
Bullet({
super.position,
}) : super(
size: Vector2(25, 50),
anchor: Anchor.center,
);
@override
Future<void> onLoad() async {
// Omitted
}
@override
void update(double dt) {
super.update(dt);
position.y += dt * -500;
if (position.y < -height) {
removeFromParent();
}
}
}
The above code should be straight forward, but lets break it down:
We add to the bullet’s y axis position at a rate of -500 pixels per second. Remember going up in the y axis means getting closer to
0since the top left corner of the screen is0, 0.If the y is smaller than the negative value of the bullet’s height, means that the component is completely off the screen and it can be removed.
Right, we now have a Bullet class ready, so lets start to implement the action of shooting.
First thing, let’s create two empty methods in the Player class, startShooting() and
stopShooting().
class Player extends SpriteAnimationComponent
with HasGameRef<SpaceShooterGame> {
// Rest of implementation omitted
void startShooting() {
// TODO
}
void stopShooting() {
// TODO
}
}
And let’s hook into those methods from the game class by using the onDragStart()
and onDragEnd() methods from the DragCallbacks mixin that we already have been using for the
ship movement:
class SpaceShooterGame extends FlameGame with DragCallbacks {
late Player player;
// Rest of implementation omitted
@override
void onDragUpdate(DragUpdateEvent event) {
player.move(event.localDelta);
}
@override
void onDragStart(DragStartEvent event) {
super.onDragStart(event);
player.startShooting();
}
@override
void onDragEnd(DragEndEvent event) {
super.onDragEnd(event);
player.stopShooting();
}
}
Note that onDragStart and onDragEnd keep track of the drag state for us, so our overrides have
to call super before doing their own work.
We now have everything set up, so let’s write the shooting routine in our player class.
Remember, the shooting behavior will be adding bullets through time intervals when the player is dragging the starship.
We could implement the time interval code and the spawning manually, but Flame
provides a component out of the box for that, the SpawnComponent, so let’s take advantage of it:
class Player extends SpriteAnimationComponent
with HasGameRef<SpaceShooterGame> {
late final SpawnComponent _bulletSpawner;
@override
Future<void> onLoad() async {
// Loading animation omitted
_bulletSpawner = SpawnComponent(
period: .2,
selfPositioning: true,
factory: (index) {
return Bullet(position: position + Vector2(0, -height / 2));
},
autoStart: false,
);
gameRef.add(_bulletSpawner);
}
void move(Vector2 delta) {
position.add(delta);
}
void startShooting() {
_bulletSpawner.timer.start();
}
void stopShooting() {
_bulletSpawner.timer.stop();
}
}
Hopefully the code above speaks for itself, but let’s look at it in more detail:
First we declared a
SpawnComponentcalled_bulletSpawnerin our game class, we needed it to be an variable accessible to the whole component since we will be accessing it in thestartShootingandstopShootingmethods.We initialize our
_bulletSpawnerin theonLoadmethod. In the first argument,period, we set how much time in seconds it will take between calls, and we choose.2seconds for now.We set
selfPositioning: trueso the spawn component doesn’t try to position the created component since we want to handle that ourselves to make the bullets spawn out of the ship.The
factoryattribute receives a function that will be called every time theperiodis
reached and return the created component.We set
autoStart: falseso it does not start by default.Finally we add the
_bulletSpawnerto our component, so it can be processed in the game loop.Note how the
_bulletSpawneris added to the game instead of the player, since the bullets are part of the whole game and not the player itself.
With the _bulletSpawner all set up, the only missing piece now is starting the
_bulletSpawner.timer in startShooting() and stopping it in the stopShooting()!
And that closes this step, putting us real close to a real game!
1import 'package:flame/components.dart';
2import 'package:flame/events.dart';
3import 'package:flame/game.dart';
4import 'package:flame/parallax.dart';
5import 'package:flutter/material.dart';
6
7void main() {
8 runApp(GameWidget(game: SpaceShooterGame()));
9}
10
11class SpaceShooterGame extends FlameGame with DragCallbacks {
12 late Player player;
13
14 @override
15 Future<void> onLoad() async {
16 final parallax = await loadParallaxComponent(
17 [
18 ParallaxImageData('assets/images/stars_0.png'),
19 ParallaxImageData('assets/images/stars_1.png'),
20 ParallaxImageData('assets/images/stars_2.png'),
21 ],
22 baseVelocity: Vector2(0, -5),
23 repeat: ImageRepeat.repeat,
24 velocityMultiplierDelta: Vector2(0, 5),
25 );
26 add(parallax);
27
28 player = Player();
29 add(player);
30 }
31
32 @override
33 void onDragUpdate(DragUpdateEvent event) {
34 player.move(event.localDelta);
35 }
36
37 @override
38 void onDragStart(DragStartEvent event) {
39 super.onDragStart(event);
40 player.startShooting();
41 }
42
43 @override
44 void onDragEnd(DragEndEvent event) {
45 super.onDragEnd(event);
46 player.stopShooting();
47 }
48}
49
50class Player extends SpriteAnimationComponent
51 with HasGameRef<SpaceShooterGame> {
52 Player()
53 : super(
54 size: Vector2(100, 150),
55 anchor: Anchor.center,
56 );
57
58 late final SpawnComponent _bulletSpawner;
59
60 @override
61 Future<void> onLoad() async {
62 await super.onLoad();
63
64 animation = await gameRef.loadSpriteAnimation(
65 'assets/images/player.png',
66 SpriteAnimationData.sequenced(
67 amount: 4,
68 stepTime: 0.2,
69 textureSize: Vector2(32, 48),
70 ),
71 );
72
73 position = gameRef.size / 2;
74
75 _bulletSpawner = SpawnComponent(
76 period: 0.2,
77 selfPositioning: true,
78 factory: (index) {
79 return Bullet(
80 position:
81 position +
82 Vector2(
83 0,
84 -height / 2,
85 ),
86 );
87 },
88 autoStart: false,
89 );
90
91 gameRef.add(_bulletSpawner);
92 }
93
94 void move(Vector2 delta) {
95 position.add(delta);
96 }
97
98 void startShooting() {
99 _bulletSpawner.timer.start();
100 }
101
102 void stopShooting() {
103 _bulletSpawner.timer.stop();
104 }
105}
106
107class Bullet extends SpriteAnimationComponent
108 with HasGameRef<SpaceShooterGame> {
109 Bullet({
110 super.position,
111 }) : super(
112 size: Vector2(25, 50),
113 anchor: Anchor.center,
114 );
115
116 @override
117 Future<void> onLoad() async {
118 await super.onLoad();
119
120 animation = await gameRef.loadSpriteAnimation(
121 'assets/images/bullet.png',
122 SpriteAnimationData.sequenced(
123 amount: 4,
124 stepTime: 0.2,
125 textureSize: Vector2(8, 16),
126 ),
127 );
128 }
129
130 @override
131 void update(double dt) {
132 super.update(dt);
133
134 position.y += dt * -500;
135
136 if (position.y < -height) {
137 removeFromParent();
138 }
139 }
140}