Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Projectiles - Spawning

Setting up

We're going to add a projectiles.rs file to hold everything directly related to projectiles. So ago ahead and create one.

Out with the old

Here's our old projectile spawning script. It was poorly named, in addition to no longer being enough for our purposes.

Old Projectile Script
srclib.rs
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,
    ));
}

First of all, as established in the plan, projectiles aren't responsible for determining what direction they go in, the patterns that spawn them will be. So our spawn_projectile function should not know or care about determining that itself. ee Really though we have a lot of data we want to capture in a projectile and no good way to store and transmit it, yet. That's what we're going to create.

srcprojectiles.rs
// We'll need this stuff later, so might as well put it in now.
use crate::config::{MovementBehavior, *};
use crate::{
    Collider, ColliderDebugRender, GravitySource, LightSource, Projectile, ScriptedMovement, Timer,
    Velocity,
};
use engine::prelude::*;

#[derive(Clone, Copy, Default)]
pub struct ProjectileTemplate {
    color: Color,
    hitbox_size: f32,
    light: LightSource,
    gravity: GravitySource,
    projectile: Projectile,
    timer: Timer,
    scripted_movement: ScriptedMovement,
}

The idea is anything we would want to predefine gets setup ahead of time in a way that makes it as easy as possible to reason about and use in our spawning function.

💡   If you're wondering where GravitySource and LightSource are from, they're from our sidequest!

Now we're trying to store information to custom components, those components must now have the Debug trait. So we add that to them all in lib.rs ie

srclib.rs
#[derive(Default, Debug)]
#[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,
}

Defining our first projectile

We'll define our projectile types as constants on the struct itself. In the future when we get more complicated we'll likely want to do something a little more sophisticated and scalable. But this works just fine for now. For this basic projectile, feel free to use whatever numbers you'd like! I changed it up a bit from the one we had earlier just because I felt like it.

srcprojectiles.rs

impl ProjectileTemplate {
    pub const BASIC_PLAYER: Self = Self {
        color: Color::new(0.8, 0.6, 0.2, 1.0),
        hitbox_size: 10.0,
        light: LightSource {
            falloff_distance: 500.0,
            strength: 4.0,
            falloff_power: 1.0,
        },
        grid_gravity: GravitySource {
            strength: 1000.0,
            falloff_distance: 250.0,
            falloff_power: 3.0,
        },
        projectile: Projectile {
            damage: 5.0,
            valid_targets: Faction::Enemy,
            max_number_of_hits: Some(1),
            number_of_hits: 0,
            on_hit_juice: [None; 0],
            on_hit_effects: [None; 0],
            on_death_juice: [None; 0],
            on_death_effects: [None; 0],
        },
        timer: Timer {
            time_remaining: 3.0,
        },
        scripted_movement: ScriptedMovement {
            movement_behavior: MovementBehavior::Linear { speed: 1000.0 },
        },
    };
}

Spawning it!

srcprojectiles.rs

pub fn fire_projectile(
    spawn_position: Vec2,
    direction: Vec2,
    projectile_information: ProjectileTemplate,
    damage_multiplier: f32,
    engine: &Engine,
) {
    let transform = &Transform {
        position: spawn_position,
        layer: PROJECTILE_LAYER,
        ..Default::default()
    };

    let color_component = &projectile_information.color;

    let hitbox = &Collider::Sphere {
        radius: (projectile_information.hitbox_size),
    };
    let hitbox_visualization = &ColliderDebugRender {
        color: projectile_information.color,
        ..Default::default()
    };

    // This is effectively incomplete until we start using other movement behavior types.
    let speed = match projectile_information.scripted_movement.movement_behavior {
        MovementBehavior::Linear { speed } => speed,
        _ => 0.0, 
    };

    let velocity = &Velocity {
        current_velocity: direction * speed,
        ..Default::default()
    };

    let projectile = &Projectile {
        damage: projectile_information.projectile.damage * damage_multiplier,
        ..projectile_information.projectile
    };

    engine.spawn(bundle!(
        transform,
        color_component,
        hitbox,
        hitbox_visualization,
        &projectile_information.light,
        &projectile_information.grid_gravity,
        velocity,
        projectile,
        &projectile_information.timer,
        &projectile_information.scripted_movement
    ));
}

Then back home in lib.rs we can update our fire_projectile_system. Don't forget to include projectiles.rs as a mod!

srcprojectiles.rs
mod projectiles;
use crate::projectiles::*;

#[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;
    };

    let direction = (player_input.aim_position - player_transform.position).normalize();

    if player_input.fire_projectile {
        fire_projectile(
            player_input.aim_position,
            direction,
            ProjectileTemplate::BASIC_PLAYER,
            1.0,
            &engine,
        );
    }
}

Now we can go ahead and implement systems for having these actually do something.