Bevy #2: Space Shooter - The Player

ethanyidong

Ethan Tang

Posted on September 26, 2020

Bevy #2: Space Shooter - The Player

NOTE: Since this has been written, Bevy has received many awesome, but sadly, breaking updates. The code in this guide is no longer maintained.

Welcome back! If you didn't see the previous article, you can find that here:

You'll need to download the assets here to follow along. (Kenney assets are great for any aspiring devs with questionable art skills, make sure to check out the rest of their stuff)
Again, setup your project the same way as the last time: cargo new <your-cool-name> and

Cargo.toml

[dependencies]
bevy = "0.2"
Enter fullscreen mode Exit fullscreen mode

Now, make a folder structure with assets/textures in your projects, and move in some images from the asset pack into this folder. You can choose whatever, but for this tutorial I'm using playerShip1_blue.png, laserRed01.png, laserBlue01.png, and enemyRed1.pngScreen Shot 2020-09-26 at 12.52.39 PM

Movement code

This is going to be a lot of repeat from last time, so here's the player movement code in full.

main.rs

use bevy::prelude::*;

struct Player {
    speed: f32, /* #1 */
}

fn main() {
    App::build()
        .add_resource(WindowDescriptor { /* #2 */
            title: "Space Shooter".to_string(),
            width: 1024,
            height: 1024,
            vsync: true,
            resizable: false,
            ..Default::default()
        })
        .add_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
        .add_default_plugins()
        .add_startup_system(setup.system())
        .add_system(player_control.system())
        .run();
}

fn setup(
    mut commands: Commands, 
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>
) {
    let player_texture_handle = asset_server.load("assets/textures/playerShip1_blue.png").unwrap();/* #3 */
    commands
        .spawn(Camera2dComponents::default())
        .spawn(SpriteComponents {
            material: materials.add(player_texture_handle.into()),
            transform: Transform::from_translation(Vec3::new(0.0, -256.0, 0.0)),
            ..Default::default()
        })
        .with(Player { 
            speed: 400.0
        });
}

fn player_control(
    time: Res<Time>, /* #4 */
    keyboard_input: Res<Input<KeyCode>>,
    mut query: Query<(&Player, &mut Transform)>,
) {
    let mut movement = 0.0;
    if keyboard_input.pressed(KeyCode::A) {
        movement -= 1.0;
    }
    if keyboard_input.pressed(KeyCode::D) {
        movement += 1.0;
    }

    for (player, mut transform) in &mut query.iter() {
        transform.translate(Vec3::new(movement * player.speed * time.delta_seconds, 0.0, 0.0)) /* #4 */
    }
}
Enter fullscreen mode Exit fullscreen mode

Most of this should be self-explanatory, except for a few key differences I want to highlight.

  1. Our player struct now has a speed property that controls its speed. We use this property within the player_control system to move the player accordingly. We could just move this to a constant instead, but this is cleaner and takes advantage of the powers of ECS.
  2. We've added in a WindowDescriptor. You can find more details here, but it basically tells the Window plugin how to construct the window. The ClearColor is changed as well, to get a black background.
  3. Here because we want to draw something other than a rectangle (boring), we have to load a handle to the texture with AssetServer, a resource added in the default plugin.
  4. Finally, we added another resource handle to our movement system because we need to use Time.delta_seconds to move our ship regardless of framerate.

Shooting

We could make the player entity directly spawn the laser bolts into the world, but the player isn't the only thing that needs to shoot. Later when we go to implement enemy shooting, we would need to copy that code over, and we'd have to edit both every time we wanted to make a change.

Luckily, ECS lets us modularize very easily by creating a new Component.

main.rs

struct Weapon {
    fired: bool,
    offset: Vec3,
    cooldown: Timer,
    material_id: usize,
}

fn main() {
    /**/
        .add_system_to_stage(stage::POST_UPDATE, weapons.system())
}

fn setup(
    /**/
) {
    /**/
        .with(Player { 
            speed: 400.0
        })
        .with(Weapon {
            fired: false,
            offset: Vec3::new(0.0, 30.0, 0.0),
            cooldown: Timer::from_seconds(0.4, false),
            material_id: 0,
        });
}

fn player_control(
    time: Res<Time>,
    keyboard_input: Res<Input<KeyCode>>,
    mut query: Query<(&Player, &mut Transform, Option<&mut Weapon>)>,
) {
    /**/

    for (player, mut transform, weapon) in &mut query.iter() {
        /**/
        if let Some(mut w) = weapon {
            w.fired = weapon_fired || w.fired;
        }
    }
}

fn weapons (
    time: Res<Time>,
    materials: Res<MaterialHandles>,
    mut query: Query<(&mut Weapon, &Transform)>,
) {
    for (mut weapon, transform) in &mut query.iter() {
        weapon.cooldown.tick(time.delta_seconds);
        if weapon.cooldown.finished && weapon.fired {
            println!("I'm firin' mah lazer");
            weapon.fired = false;
            weapon.cooldown.reset();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

There are three new things introduced here: Timer, Option<Component> and stages.
Timer is a helper struct provided by bevy that doesn't do much but keep track of elapsed time and whether or not it is more than the specified length. This is extremely useful for weapon cooldowns.
Optional components let us set a component as optional within the query. This lets us support things in the future (if we decide to make a player without weapons)
stages let us control when bevy executes each system. By default, all systems are in stage::UPDATE, so by placing our weapon system in stage::POST_UPDATE, we can fire our weapon the same "frame" as the button was pressed.

UPDATE: Do not follow the stage code above, adding the weapons system to the post_update stage causes some weird stalling behavior the first time it is fired. Just use add_system as normal.

Lasers, lasers, lasers!

Obviously, we want to do more than just print out a message when we fire our laser, so let's add in our lasers. But first, let's talk about Assets. Assets<T> is basically a HashMap<Handle<T>, T>. When we run materials.add(), we are returned a new handle every time. If we do this for the lasers, every time we want to shoot we'd have to load in the texture again. To avoid this, we have to store our Handles as a resource.

main.rs

struct MaterialHandles(Vec<Handle<ColorMaterial>>);

fn setup(
    /**/
) -> {
    let laser_texture_handle = asset_server.load("assets/textures/laserBlue01.png").unwrap();
    commands
        .insert_resource(MaterialHandles(vec![materials.add(laser_texture_handle.into())]))
}

Enter fullscreen mode Exit fullscreen mode

Now let's add the code to insert our lasers into the World. We need to access Commands, a thread-safe queue of operations on the ECS World in our weapons system, just like we did in our startup system.

main.rs

struct Laser {
    speed: f32,
}

fn weapons (
    mut commands: Commands, /* this must be the fist argument for bevy to recognize this as a system */
    time: Res<Time>,
    materials: Res<MaterialHandles>,
    mut query: Query<(&mut Weapon, &Transform)>,
) {
    for (mut weapon, transform) in &mut query.iter() {
        weapon.cooldown.tick(time.delta_seconds);
        if weapon.cooldown.finished && weapon.fired {
            commands
                .spawn(SpriteComponents {
                    material: materials.0[weapon.material_id],
                    transform: Transform::from_translation(weapon.offset + transform.translation()),
                    ..Default::default()
                })
                .with(Laser {
                    speed: 1000.0
                });
            weapon.fired = false;
            weapon.cooldown.reset();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can add a movement system for lasers.

main.rs

fn main() {
    /**/
        .add_system(player_control.system())
        .add_system(laser_move.system())
        .add_system_to_stage(stage::POST_UPDATE, weapons.system())
}

fn laser_move(
    time: Res<Time>,
    mut query: Query<(&Laser, &mut Transform)>
) {
    for (laser, mut transform) in &mut query.iter() {
        transform.translate(Vec3::new(0.0, laser.speed * time.delta_seconds, 0.0) )
    }
}
Enter fullscreen mode Exit fullscreen mode

Aaaand we're done! Thanks for following along! You can find the code for this tutorial here:

In the next article (WIP), we'll add in our enemies.

💖 💪 🙅 🚩
ethanyidong
Ethan Tang

Posted on September 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related