Projectiles (Setup)
The projectile tag we made earlier isn't going to cut it for our much broader plan anymore.
Let's go step by step through our plan and make sure we set ourselves up to do all the things correctly.
1) Hitbox
How big is the active hitbox of the projectile, and what shape is it?
To do this we're going to use collision, which means importing the physics package of bloop into our project. So we need to update our dependencies:
[dependencies]
engine.git = "https://github.com/bloopgames/bloop-rs.git"
physics = { git = "https://github.com/bloopgames/bloop-rs.git", package = "physics-rs" }
serde = { version = "1.0", features = ["derive"] }
This gives us access to the Collider and ColliderDebugRender components, we'll want to use both for our projectiles.
Components to include in a projectile
Collider, ColliderDebugRender
2) Visualization
What does the projectile look like? Do the visuals need to be animated? Does it have a trail of some kind? Is there an effect that plays when the projectile hits something / ends its existence?
For now we're going to use the aforementioned ColliderDebugRender for what our projectiles look like. We won't be animating them or adding a trail. For whether or not there's an effect at the end of its life or when it hits something, we'll put in an on_hit_juice and on_death_juice for the projectile. We want to leave room to expand/overngineer things, so we'll implement a JuiceEffect enum for any type of juicy effects we may want to trigger. I find it easier to track these things in config.rs, so that's where I'll do it.
If you're unfamiliar with the term "juice," popularized by this legendary presentation, it's a reference to "juicing up" interactions in your game, giving them more oomph and impact by doing cosmetic things like screenshake or spawning particles.
// Because we'll be using this within components, we need all these traits
// besides Default which is just hand to have.
#[derive(Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum JuiceEffect {
#[default]
None,
Particles { amount: u32, size: f32, },
ScreenShake { intensity: f32, duration: f32 }, // <- We won't implement this yet
// etc.
}
We'll expand our Projectile component to be more than just a tag. We're setting the max amount of each effect to 0 for now, effectively making them pointless. Remember, we're just laying scaffolding, these further details will get fleshed out when we're ready for them.
#[derive(Default)]
#[component]
struct Projectile{
on_hit_juice: [Option<JuiceEffect>; 0],
on_death_juice: [Option<JuiceEffect>; 0],
}
Components to include in a projectile
Collider, ColliderDebugRender, Projectile
3) Lifetime
How long does the projectile stay in existence? How many things does it have to hit before it dies?
We can continue to use our Timer component for the time aspect. For the amount of "hits before despawning" we can make sure our Projectile component has an option for max number of hits, as well as a count for the current number of hits.
#[derive(Default)]
#[component]
struct Projectile{
on_hit_juice: [Option<JuiceEffect>; 0],
on_death_juice: [Option<JuiceEffect>; 0],
max_number_of_hits: Option<u32>,
number_of_hits: u32,
}
Components to include in a projectile
Collider, ColliderDebugRender, Timer, Projectile
4) Interaction Behavior
What happens when it hits something? Can it hit the same thing more than once? What can it hit, and how much damage does it do to them?
We've set up on hit cosmetic effects with JuiceEffects but not gameplay ones. We'll set up a mirror to that in GameEffects.
#[derive(Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum GameEffect {
#[default]
None,
Knockback,
SpawnProjectile,
SpawnPattern,
ApplyDebuff,
}
Like JuiceEffects we're putting them in now but won't have these fully integrated just yet, but you can probably already imagine how they'll work. For determining if we can hit the same entity more than once with the same projectile, we'll need to punt on that for now as I don't actually believe it's worth the even minor performance cost to care about. We'll just say "yes."
For what a projectile can hit, a valid_targets field will do the trick, alongside a new Faction enum.
#[derive(Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum Faction {
#[default]
All,
Enemy,
Player,
}
Damage, of course, can be represented by a number - we'll use a float so we can apply percentage based modifiers to it easily. So with the above setup we add to our Projectile component:
#[derive(Default)]
#[component]
struct Projectile{
damage: f32,
valid_targets: Faction,
on_hit_juice: [Option<JuiceEffect>; 0],
on_hit_effects: [Option<GameEffect>; 0],
on_death_juice: [Option<JuiceEffect>; 0],
on_death_effects: [Option<GameEffect>; 0],
max_number_of_hits: Option<u32>,
number_of_hits: u32,
}
Components to include in a projectile
Collider, ColliderDebugRender, Timer, Projectile
5) Movement
How does it move? How much velocity does it have, in what direction, and does that change over time?
Here it maybe tempting to lean into ECS and handle this outside of our Projectile component. Like we currently have update_player_acceleration and update_enemy_acceleration we can similarly have a unique function for every single type of projectile behavior we want. Then when we go to spawn projectiles, we can have a unique function for spawning every different set of components we want on a projectile.
Oh, wait. That's a nightmare!
Instead we'll introduce a new ScriptedMovement component for all our "dumb" entities that are given a (possibly complex) movement behavior, not just projectiles. We'll still combine this with a Velocity component so all our movement can be handled in the same place, but we'll also introduce a MovementBehavior enum that gets processed earlier in the frame to adjust the velocity to match our design.
#[derive(Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum MovementBehavior {
#[default]
Linear {
speed: f32,
},
Homing,
Boomerang,
Orbital,
Sine,
PerpendicularSine,
}
#[component]
struct ScriptedMovement {
movement_behavior: MovementBehavior
}
Components to include in a projectile
Collider, ColliderDebugRender, Timer, Projectile, ScriptedMovement, Velocity
6) Sound
We can't yet, but if we could it would be as simple as adding that as a type of JuiceEffect.
Putting it all together
Alright now let's get to work putting together a function to actually spawn these monstrosities!