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.
class Bullet extends SpriteAnimationComponent
with HasGameReference<SpaceShooterGame> {
Bullet({
super.position,
}) : super(
size: Vector2(25, 50),
anchor: Anchor.center,
);
@override
Future<void> onLoad() async {
await super.onLoad();
animation = await game.loadSpriteAnimation(
'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 HasGameReference<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
0
since 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 HasGameReference<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 onPanStart()
and onPanEnd()
methods from the PanDetector
mixin that we already have been using for the ship
movement:
class SpaceShooterGame extends FlameGame with PanDetector {
late Player player;
// Rest of implementation omitted
@override
void onPanUpdate(DragUpdateInfo info) {
player.move(info.delta.global);
}
@override
void onPanStart(DragStartInfo info) {
player.startShooting();
}
@override
void onPanEnd(DragEndInfo info) {
player.stopShooting();
}
}
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 HasGameReference<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,
),
);
return bullet;
},
autoStart: false,
);
game.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
SpawnComponent
called_bulletSpawner
in our game class, we needed it to be an variable accessible to the whole component since we will be accessing it in thestartShooting
andstopShooting
methods.We initialize our
_bulletSpawner
in theonLoad
method. In the first argument,period
, we set how much time in seconds it will take between calls, and we choose.2
seconds for now.We set
selfPositioning: true
so 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
factory
attribute receives a function that will be called every time theperiod
is
reached and return the created component.We set
autoStart: false
so it does not start by default.Finally we add the
_bulletSpawner
to our component, so it can be processed in the game loop.Note how the
_bulletSpawner
is 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/input.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 PanDetector {
13 late Player player;
14
15 @override
16 Future<void> onLoad() async {
17 final parallax = await loadParallaxComponent(
18 [
19 ParallaxImageData('stars_0.png'),
20 ParallaxImageData('stars_1.png'),
21 ParallaxImageData('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
33 @override
34 void onPanUpdate(DragUpdateInfo info) {
35 player.move(info.delta.global);
36 }
37
38 @override
39 void onPanStart(DragStartInfo info) {
40 player.startShooting();
41 }
42
43 @override
44 void onPanEnd(DragEndInfo info) {
45 player.stopShooting();
46 }
47}
48
49class Player extends SpriteAnimationComponent
50 with HasGameReference<SpaceShooterGame> {
51 Player()
52 : super(
53 size: Vector2(100, 150),
54 anchor: Anchor.center,
55 );
56
57 late final SpawnComponent _bulletSpawner;
58
59 @override
60 Future<void> onLoad() async {
61 await super.onLoad();
62
63 animation = await game.loadSpriteAnimation(
64 'player.png',
65 SpriteAnimationData.sequenced(
66 amount: 4,
67 stepTime: 0.2,
68 textureSize: Vector2(32, 48),
69 ),
70 );
71
72 position = game.size / 2;
73
74 _bulletSpawner = SpawnComponent(
75 period: 0.2,
76 selfPositioning: true,
77 factory: (index) {
78 return Bullet(
79 position: position +
80 Vector2(
81 0,
82 -height / 2,
83 ),
84 );
85 },
86 autoStart: false,
87 );
88
89 game.add(_bulletSpawner);
90 }
91
92 void move(Vector2 delta) {
93 position.add(delta);
94 }
95
96 void startShooting() {
97 _bulletSpawner.timer.start();
98 }
99
100 void stopShooting() {
101 _bulletSpawner.timer.stop();
102 }
103}
104
105class Bullet extends SpriteAnimationComponent
106 with HasGameReference<SpaceShooterGame> {
107 Bullet({
108 super.position,
109 }) : super(
110 size: Vector2(25, 50),
111 anchor: Anchor.center,
112 );
113
114 @override
115 Future<void> onLoad() async {
116 await super.onLoad();
117
118 animation = await game.loadSpriteAnimation(
119 'bullet.png',
120 SpriteAnimationData.sequenced(
121 amount: 4,
122 stepTime: 0.2,
123 textureSize: Vector2(8, 16),
124 ),
125 );
126 }
127
128 @override
129 void update(double dt) {
130 super.update(dt);
131
132 position.y += dt * -500;
133
134 if (position.y < -height) {
135 removeFromParent();
136 }
137 }
138}