Getting killed is not cool. On the other hand, killing monsters is. This article will implement shooting bullets and make them kill monsters on contact.
Components
Let's start by adding the required components:
#[derive(Copy, Clone)]
pub enum GameDirection {
Left,
Right
}
pub struct Bullet;
The Bullet
component will be used to identify our bullets entities.
The GameDirection
will be used to fire our bullets in the right direction.
Drawing Bullets
Let's create a file called bullets.rs
that will contain the required code to create a new bullet:
pub struct BulletOptions {
pub x: f32,
pub y: f32,
pub direction: GameDirection,
}
pub fn insert_bullet_at(
commands: &mut Commands,
materials: &Res<Materials>,
options: BulletOptions,
) {
let speed = match options.direction {
GameDirection::Left => -14.0,
_ => 14.0,
};
let x = match options.direction {
GameDirection::Left => options.x - 1.,
_ => options.x + 1.,
};
let rigid_body = RigidBodyBundle {
position: Vec2::new(x, options.y).into(),
velocity: RigidBodyVelocity {
linvel: Vec2::new(speed, 0.0).into(),
..Default::default()
},
mass_properties: RigidBodyMassPropsFlags::ROTATION_LOCKED.into(),
activation: RigidBodyActivation::cannot_sleep(),
forces: RigidBodyForces {
gravity_scale: 0.,
..Default::default()
},
..Default::default()
};
let collider = ColliderBundle {
shape: ColliderShape::cuboid(0.25, 0.05),
flags: ColliderFlags {
active_events: ActiveEvents::CONTACT_EVENTS,
..Default::default()
},
..Default::default()
};
let sprite = SpriteBundle {
material: materials.bullet_material.clone(),
sprite: Sprite::new(Vec2::new(0.5, 0.1)),
..Default::default()
};
commands
.spawn_bundle(sprite)
.insert_bundle(rigid_body)
.insert_bundle(collider)
.insert(RigidBodyPositionSync::Discrete)
.insert(Bullet);
}
This code contains the usual boilerplate to create a new entity. It creates a rigid body, a collider and a sprite. The collider must enable the CONTACT_EVENTS
flag. The color of our bullets will be yellow. Here is the code to declare the bullet material:
// src/game/mod.rs
bullet_material: materials.add(Color::rgb(0.8, 0.8, 0.).into()),
// src/game/components.rs
pub struct Materials {
pub player_material: Handle<ColorMaterial>,
pub floor_material: Handle<ColorMaterial>,
pub monster_material: Handle<ColorMaterial>,
pub bullet_material: Handle<ColorMaterial>,
}
Shooting Bullets
In our player.rs
file, we can add a system that will create a new bullet whenever the space bar is pressed:
// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
.with_system(fire_controller.system())
// system implementation
pub fn fire_controller(
keyboard_input: Res<Input<KeyCode>>,
mut commands: Commands,
materials: Res<Materials>,
players: Query<(&Player, &RigidBodyPosition), With<Player>>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
for (player, position) in players.iter() {
let options = BulletOptions {
x: position.position.translation.x,
y: position.position.translation.y,
direction: GameDirection::Right,
};
insert_bullet_at(&mut commands, &materials, options)
}
}
}
Compiling and running the game should show that pressing space shoots useless bullets. Useless since they do not kill monsters...
Killing Monsters
To kill monsters, we can add a system that detects contacts between monsters and bullets:
// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
.with_system(kill_on_contact.system())
// system implementation
pub fn kill_on_contact(
mut commands: Commands,
bullets: Query<Entity, With<Bullet>>,
enemies: Query<Entity, With<Enemy>>,
mut contact_events: EventReader<ContactEvent>,
) {
for contact_event in contact_events.iter() {
if let ContactEvent::Started(h1, h2) = contact_event {
for bullet in bullets.iter() {
for enemy in enemies.iter() {
if (h1.entity() == bullet && h2.entity() == enemy)
|| (h1.entity() == enemy && h2.entity() == bullet)
{
commands.entity(enemy).despawn_recursive();
}
}
}
}
}
}
Try running the game and try shooting at monsters. Monsters should now disappear when they are hit by a bullet.
Destroying Bullets
Our bullets are currently eternal. They do not disappear after hitting a monster, and they get stuck on walls. We should despawn them whenever they hit something:
// in build method of PlayerPlugin
SystemSet::on_update(AppState::InGame)
.with_system(destroy_bullet_on_contact.system())
// system implementation
pub fn destroy_bullet_on_contact(
mut commands: Commands,
bullets: Query<Entity, With<Bullet>>,
mut contact_events: EventReader<ContactEvent>,
) {
for contact_event in contact_events.iter() {
if let ContactEvent::Started(h1, h2) = contact_event {
for bullet in bullets.iter() {
if h1.entity() == bullet || h2.entity() == bullet {
commands.entity(bullet).despawn_recursive();
}
}
}
}
}
Shooting Left
Our player always shoots in a single direction... It would be better if we could select the direction we shoot to. To do so, we will add a facing direction to our player:
// src/game/components.rs
pub struct Player {
pub speed: f32,
pub facing_direction: GameDirection,
}
And initialize it on our player:
.insert(Player {
speed: 7.,
facing_direction: GameDirection::Right,
})
Then, we will update our player_controller
system to update this facing direction when our player moves:
pub fn player_controller(
keyboard_input: Res<Input<KeyCode>>,
mut players: Query<(&mut Player, &mut RigidBodyVelocity)>,
) {
for (mut player, mut velocity) in players.iter_mut() {
if keyboard_input.pressed(KeyCode::Left) {
velocity.linvel = Vec2::new(-player.speed, velocity.linvel.y).into();
player.facing_direction = GameDirection::Left
}
if keyboard_input.pressed(KeyCode::Right) {
velocity.linvel = Vec2::new(player.speed, velocity.linvel.y).into();
player.facing_direction = GameDirection::Right
}
}
}
Finally, we will pass this direction to our bullet creation function to fire the bullet in the right direction:
pub fn fire_controller(
keyboard_input: Res<Input<KeyCode>>,
mut commands: Commands,
materials: Res<Materials>,
players: Query<(&Player, &RigidBodyPosition), With<Player>>,
) {
if keyboard_input.just_pressed(KeyCode::Space) {
for (player, position) in players.iter() {
let options = BulletOptions {
x: position.position.translation.x,
y: position.position.translation.y,
direction: player.facing_direction,
};
insert_bullet_at(&mut commands, &materials, options)
}
}
}
Running the game should confirm that you can shoot bullets left and right.
The final code is available here.
Top comments (1)
The 30-30 ammo 500 rounds is a popular choice for hunters and sport shooters. With a box of 500 rounds, you'll have plenty of ammunition to practice your shooting skills or take on extended hunting trips. Its reliable performance and effectiveness make it a trusted option for various shooting applications.