Bullet hell
It is slightly unfair that our poor circle isn’t able to defend itself against the terrifying squares. So it’s time to implement the ability for the circle to shoot bullets.
Implementation
Dead or alive?
To keep track of which squares have been hit by bullets we add the field
collided
of the type bool
to the struct Shape
.
struct Shape {
size: f32,
speed: f32,
x: f32,
y: f32,
collided: bool,
}
Keeping track
We need another vector to keep track of all the bullets. For simplicity’s sake
we’ll call it bullets
. Add it after the squares
vector. Here we’ll also
set the type of the elements to ensure that the Rust compiler knows what type
it is before we have added anything to it. We’ll use the struct Shape
for
the bullets as well.
let mut bullets: Vec<Shape> = vec![];
Shoot bullets
After the circle has moved we’ll add a check if the player has pressed the
space key and add a bullet to the bullets
vector. The x
and y
coordinates of the bullet are set to the same values as for the circle, and
the speed is set to twice that of the circle.
if is_key_pressed(KeyCode::Space) {
bullets.push(Shape {
x: circle.x,
y: circle.y,
speed: circle.speed * 2.0,
size: 5.0,
collided: false,
});
}
Note that we’re using the function is_key_pressed()
which only returns true
during the frame when the key was first pressed.
Since we added a new field to the Shape
struct we’ll need to set it when we
create a square.
squares.push(Shape {
size,
speed: rand::gen_range(50.0, 150.0),
x: rand::gen_range(size / 2.0, screen_width() - size / 2.0),
y: -size,
collided: false,
});
Move bullets
We don’t want the bullets to be stationary mines, so we’ll have to loop over
the bullets
vector and move them in the y
direction. Add the following
code after the code that moves the squares.
for square in &mut squares {
square.y += square.speed * delta_time;
}
for bullet in &mut bullets {
bullet.y -= bullet.speed * delta_time;
}
Remove bullets and squares
Make sure to remove the bullets that have exited the screen in the same way that the squares are removed.
bullets.retain(|bullet| bullet.y > 0.0 - bullet.size / 2.0);
Now it is time to remove all the squares and bullets that have collided. It
can be done with the retain
method on the vectors which takes a predicate
that should return true
if the element should be kept. We’ll just check
whether the collided
field on the struct is false. Do the same thing for
both the squares
and the bullets
vectors.
squares.retain(|square| !square.collided);
bullets.retain(|bullet| !bullet.collided);
Collision
After the check if the circle has collided with a square we’ll add another
check if any of the squares have been hit by a bullet. We’ll set the field
collided
to true for both the square and the bullet so that they can be
removed.
for square in squares.iter_mut() {
for bullet in bullets.iter_mut() {
if bullet.collides_with(square) {
bullet.collided = true;
square.collided = true;
}
}
}
Clear bullets
When the game is over we also have to clear the bullets
vector so that all
the bullets are removed when a new game is started.
if gameover && is_key_pressed(KeyCode::Space) {
squares.clear();
bullets.clear();
circle.x = screen_width() / 2.0;
circle.y = screen_height() / 2.0;
gameover = false;
}
Draw bullets
Before the circle is drawn we’ll draw all the bullets that the player has shot. This ensures that they are drawn behind all the other shapes.
for bullet in &bullets {
draw_circle(bullet.x, bullet.y, bullet.size / 2.0, RED);
}
The is another function called
draw_circle_lines()
that can be used to draw a circle with just the outline.
This is all the code that is needed for the circle to be able to shoot down all the fearsome squares.
To increase the difficulty it’s possible to add a minimum time for reloading
between each shot. Try using the function
get_time()
to save when the last shot was fired and compare it with the current time.
Only add a bullet if the difference is above a certain threshold.
Another possibility is to only allow a specific number of bullets on the screen at the same time.