Fire a Projectile
We're not going to get too in depth here, because we're going to be expanding this greatly in the next chapter.
But we want to feel like a game first!
Fire Input
The simplest thing to get out of the way first, of course, is to detect when we should fire.
For our movement axes we've been using [button].pressed() to tell if it's pressed or not. This is great for continuous things like movement. If we were to check just if a button is pressed or not for firing, without implementing some kind of cooldown then it would fire every single frame. We'll do that eventually but for today we're going to use [button].just_pressed() instead which is only true the frame a button is pressed. So to our PlayerInput resource we'll add a fire_projectile bool and update it when we update the rest of those things.
All in lib.rs
#[derive(Default)]
#[resource]
struct PlayerInput {
move_axes: Vec2,
aim_position: Vec2,
fire_projectile: bool,
}
#[system]
fn update_player_input(
mut player_input: Mut<PlayerInput>,
input: Ref<InputState>,
camera: Query<(Ref<Transform>, Ref<Camera>)>,
window: Ref<Window>,
) {
// [...] previous code
player_input.fire_projectile = input.mouse.buttons[MouseButton::Left].just_pressed();
}
Doing the Firing
Okay, so now we have a bool in our PlayerInput resource that tracks whether or not we clicked the left mouse button. So what are we going to do with that? Add a system, of course. It'll be simple - place it after we update input, and if fire_projectile is true, do so.
#[system]
fn fire_projectile_system(
player_input: Ref<PlayerInput>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
engine: Ref<Engine>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
if player_input.fire_projectile {
fire_projectile(
player_input.aim_position,
player_transform.position,
&engine,
);
}
}
To fire the projectile, we just spawn it and give it a Velocity component. Since we already set that up to handle movement, we don't have to do any more work than that.
I'll be including a LightSource and GravitySource from our sidequest, as well, because it's awesome. Unfortunately it's also not free, performance wise. There's no reason to keep them around for longer than a few seconds when they're thousands of units away. Because we'll be later adding a bunch of things that have to have a timer, we'll first add a Timer component and a system that counts them donw. Then, we'll be adding a Projectile tag component and a system that clears those up after their timer runs out.
So for timers it's quite simple
struct Timer {
time_remaining: f32,
}
//Place this system early, perhaps at the top of the order:
#[system]
fn update_timers(mut timers: Query<Mut<Timer>>, frame_constants: Ref<FrameConstants>) {
for mut timer in timers.iter_mut() {
if timer.time_remaining > 0.0 {
timer.time_remaining -= frame_constants.delta_time;
}
}
}
This takes all timers and counts themd own by the delta_time, if they have more than 0 seconds remaining. They may or may not end up negative because of this, so we'll take that into account with our systems that use them.
For the Projectile spawning:
#[component]
struct Projectile;
fn fire_projectile(aim_position: Vec2, player_position: Vec2, engine: &Engine) {
let direction = (aim_position - player_position).normalize();
let transform = &Transform {
position: player_position,
layer: 10., // We want this to be very visible on top of most everything else.
..Default::default()
};
let color = &Color::RED;
let circle_render = &CircleRender {
num_sides: 20,
size: Vec2::splat(15.0),
visible: true,
};
let light = &LightSource {
falloff_distance: 100.,
strength: 100.,
falloff_power: 1.5,
};
let grid_gravity = &GravitySource {
strength: 1000.,
falloff_distance: 100.,
falloff_power: 1.5,
};
let velocity = &Velocity {
current_velocity: direction * 2000.,
max_velocity: 2000.,
};
let projectile = &Projectile;
let timer = &Timer { time_remaining: 2.0 };
engine.spawn(bundle!(
transform,
color,
circle_render,
light,
grid_gravity,
velocity,
projectile,
timer,
));
}
And, finally, the system to clean up projectiles...
#[system]
fn update_projectiles(
projectiles: Query<(Ref<Projectile>, Ref<EntityId>, Ref<Timer>)>,
engine: Ref<Engine>,
) {
for (_, entity_id, timer) in projectiles.iter() {
if timer.time_remaining <= 0.0 {
engine.despawn(*entity_id);
}
}
}
Put it all together, you should now be able to click and shoot projectiles that clean themselves up after a while. They're dyanmic and fun with the grid systems we have. And they're ripe for expansion in the next chapter, where we actually implement a combat system. Hop right into chapter 3 or review what my files look like at this point in our appendix for this chapter.