This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Hands-On Rust


Targeting

Now that you can rescue colonists, and they can tell you about their awful day - it's time to start adding an element of risk to the game. I very much see the game as a "bug hunt" - you find monsters and shoot them.

Let's Start by Fixing a Bug

A "bug hunt" is a good description of most time-trial projects. You spend a surprising amount of time finding bugs and mistakes you made in your hasty rush to enter some code!

In the player logic, we'd carefully only run systems like field-of-view is the game state was not equal to Wait. In other words, only if the player was executing an action. Events weren't firing in quite the order I wanted, so I changed all instances of if *new_state != NewState::Wait to if *new_state == NewState::Wait in player.rs - to make them fire when the user has stopped mashing keys.

Create a Name Component

We want to start naming things, so we have a name to display in the targeting panel. Create a new component file named src/components/name.rs:


#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct Name(pub String);
}

Don't forget to add mod name; pub use name::*; to components/mod.rs.

In map/layerbuilder/colonists.rs add a Name component to each of the colonists you are spawning:


#![allow(unused)]
fn main() {
Name("Colonist".to_string()),
}

It belongs in the push statement for both spawn_random_colonist and spawn_first_colonist.

You also want to open main.rs and give SecBot a name:


#![allow(unused)]
fn main() {
self.ecs.push((
    Player {},
    Name("SecBot".to_string()),
    ...
}

Adding a Monster

The first thing we need is a way to tell the game that a monster is, in fact, a hostile entity. Open up src/components/tags.rs and add another blank structure:


#![allow(unused)]
fn main() {
pub struct Hostile;
}

We're already including tags::* in the module file, so we don't need to add any includes.

Now we want to be able to spawn a monster. We're not going to give it much detail yet, just make it exist. Create a new file, src/map/layerbuilder/monsters.rs. This will contain our monster spawning code:


#![allow(unused)]
fn main() {
use crate::components::*;
use bracket_lib::prelude::*;
use legion::*;

pub fn spawn_face_eater(ecs: &mut World, location: Point, layer: u32) {
    ecs.push((
        Name("Face Eater".to_string()),
        Hostile {},
        Position::with_pt(location, layer),
        Glyph {
            glyph: to_cp437('f'),
            color: ColorPair::new(RED, BLACK),
        },
        Description("Nasty eight-legged beastie that likes to eat faces.".to_string()),
    ));
}
}

The monster shares a lot of functionality with other game elements - it has a position, render information, a description, and a name. It also has the new Hostile tag component, to indicate that it is a baddie. Open src/map/layerbuilder/mod.rs and include pub mod monsters; - this includes the module in the game.

Open src/map/layerbuilder/entrance.rs. In your includes, add use super::monsters::* to include everything from the monsters file. Then find the populate_rooms function and change the random colonist spawner to sometimes spawn a monster instead:


#![allow(unused)]
fn main() {
ooms.iter().skip(1).for_each(|r| {
    if rng.range(0, 5) == 0 {
        spawn_random_colonist(ecs, r.center(), 0);
    } else {
        spawn_face_eater(ecs, r.center(), 0);
    }
});
}

That's enough that if you run the game you'll sometimes find a monster in a room. You can't interact with it, but it's there. The nasty f at least looks menacing!

Targeting

I decided that the next step was to support a targeting system, ready for slaying monsters. Let's start that process.

Create a Targeting Component

Create a new file, src/components/targeting.rs. Add the following code to it:


#![allow(unused)]
fn main() {
use legion::Entity;

pub struct Targeting {
    pub targets: Vec<(Entity, f32)>, // (entity / distance)
    pub current_target: Option<Entity>,
    pub index: usize,
}
}

What we're doing here is storing a vector of all possible targets; it contains tuples of the entity itself and the distance to the entity. Then we store an option for current_target - there might not be one, if there is we'll store its Entity entry. Finally, index stores the index of the current target in the targeting list. We'll be using that for target cycling.

Don't forget to add mod targeting; pub use targeting::*; to your components/mod.rs file.

Rendering Target Information

Make another new file, this time named src/render/targeting_panel.rs. It's contains a large blob of code, so let's walk through each chunk.


#![allow(unused)]
fn main() {
use crate::components::*;
use bracket_lib::prelude::*;
use legion::*;

use super::WIDTH;

pub fn render_targeting_panel(
    mut y: i32,
    ctx: &mut BTerm,
    ecs: &World,
    _current_layer: usize,
) -> (i32, Option<Point>) {
}

This imports necessary modules, and sets up the function. We're accepting y to indicate where we should start rendering in the right panel. Note that y is mutable - but it isn't a reference. This allows us to change our copy of y inside the function without changing the original that was passed in.

We're also ignoring the current_layer variable. I was pretty sure I'd need it later.

The function returns a tuple containing an i32 (which will specify where we are on the vertical axis, since targeting may be of variable size) and an Option<Point> which will specify where the target is - if one exists.


#![allow(unused)]
fn main() {
    let mut target_point = None;
    let x = WIDTH + 3;
    let mut tq = <(&Player, &Targeting)>::query();
    let current_target = tq.iter(ecs).map(|(_, t)| t.current_target).nth(0).unwrap();
}

Here we set target_point to None. We don't know if there is one, yet. Then we set x to WIDTH plus three - the x coordinate at which we can start adding to the panel. Then we query the ECS for entities that have both a Player and a Targeting component (we'll add that to the player soon). There should only be one, so we iterate the query, use map to extract just the current_target field, and retrieve the 1st (0) result. So now current_target contains either None or the Entity we are targeting.


#![allow(unused)]

fn main() {
    ctx.print_color(x, y, GREY, BLACK, "----------------------------");
    y += 1;

}

Print a line to break up the panel a bit, and increment y.


#![allow(unused)]

fn main() {
    if let Some(target_entity) = current_target {
}

Use if let (it's a single option form of match) to extract target_entity if there is one. If there isn't, this block of code won't run at all.


#![allow(unused)]
fn main() {
        // TODO: Retrieve target details here
        if let Ok(entry) = ecs.entry_ref(target_entity) {
            if let Ok(name) = entry.get_component::<Name>() {
}

Use if let once more, to see if entry_ref and get_component return information about our target. entry_ref is Legion's method of saying "I want to inspect this entity" - and returns a handle you can use to access the entity's components. get_component will return a reference to the Name component if there is one.


#![allow(unused)]
fn main() {
                ctx.print_color(
                    x,
                    y,
                    RED,
                    BLACK,
                    format!("Target: {}", name.0.to_uppercase()),
                );
                y += 1;
            }
            if let Ok(pos) = entry.get_component::<Position>() {
                target_point = Some(pos.pt);
            }
        }
}

Armed with the name, we can print the target's name (if there is one). We then use the same get_component setup (using the entry) to see if the target has a position. If it does, we set target_point to that location.


#![allow(unused)]
fn main() {
        ctx.print_color(x, y, GOLD, BLACK, "T to cycle targets");
        y += 1;
        ctx.print_color(x, y, GOLD, BLACK, "F to fire");
        y += 1;
    } else {
        ctx.print_color(x, y, GREEN, BLACK, "No current target");
        y += 1;
    }

    (y, target_point)
}
}

Lastly, we print some helpful information for the user - and return y and the target_point.

Don't forget to add pub mod targeting_panel; pub use targeting_panel::*; to src/render/mod.rs.

Note that I initially went with TAB for target cycling. Running the game in Chrome showed why this was a bad idea. It would work, but also sometimes change the currently highlighted UI element to the browser navigation bar - which made pressing keys a bad idea. I changed it to T.

Rendering the UI, Redux

Now that we've created our targeting panel, we need to use it. We've started down the road of creating dynamically positioned panel elements, so let's build on that. Open main.rs and find the tick function. We're going to modify the UI so that each element returns position information when it finishes - allowing the UI to flow smoothly by itself:


#![allow(unused)]
fn main() {
impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        render::render_ui_skeleton(ctx);
        let y = render::render_colonist_panel(ctx, &self.ecs, self.map.current_layer);
        let (y, target_pt) = render::render_targeting_panel(y, ctx, &self.ecs, self.map.current_layer);
        self.map.render(ctx);
}

This requires that we modify the render_colonist_panel to return a y value. Open src/render/colonist_panel.rs. There are two changes to make. The first tells the function to return an i32 (the y value):


#![allow(unused)]
fn main() {
pub fn render_colonist_panel(ctx: &mut BTerm, ecs: &World, current_layer: usize) -> i32 {
}

Then as the last line of the function, you want to return the y value we used when rendering the panel:


#![allow(unused)]
fn main() {
    y
}
}

Giving the Player a Target

Also in main.rs, we want to expand the Player initialization to include a Targeting list:

TODO: Expand this snippet


#![allow(unused)]
fn main() {
fn new_game(&mut self) {
    use components::*;

    // Spawn the player
    self.ecs.push((
        Player {},
        Position::with_pt(self.map.get_current().starting_point, 0),
        Glyph {
            glyph: to_cp437('@'),
            color: ColorPair::new(YELLOW, BLACK),
        },
        Description("Everybody's favorite Bracket Corp SecBot".to_string()),
        FieldOfView{radius: 20, visible_tiles: HashSet::new()},
        Targeting {
            targets: Vec::new(),
            current_target: None,
            index: 0,
        },
    ));
}
}

We also want to correct an issue that would prevent the visible area from rendering in the first turn. Immediately after you push the new player, call:


#![allow(unused)]
fn main() {
// Trigger FOV for the first round
game::player::update_fov(&NewState::Enemy, &mut self.ecs, &mut self.map);
}

Pop into game/player.rs and change fn update_fov to pub fn update_fov to allow you to call it from main.

Finding a Target when you Move

Now we need to modify player behavior to include targeting. The first step is to pick a target when the player moves; new targets may become available, and the player probably wants to shoot one. We'll start by storing some data we need when we query the player:


#![allow(unused)]
fn main() {
let mut visible = None;
let mut player_pos = Point::zero();
let mut player_entity = None;

// Build the player FOV
let mut query = <(Entity, &Player, &Position, &mut FieldOfView)>::query();
query.for_each_mut(ecs, |(e, _, pos, fov)| {
    player_pos = pos.pt;
    player_entity = Some(*e);
    ...
}

When we query the player's information, we store the entity and position for use further along in the function. There's no point in finding it twice! Next, find the update_fov function, and we'll add some new behavior:


#![allow(unused)]
fn main() {
 if let Some(vt) = visible {
    // Update colonist status
    ...

    // Targeting system
    let mut possible_targets = <(Entity, &Hostile, &Position)>::query();
    let mut targets = possible_targets
        .iter(ecs)
        .filter(|(_, _, pos)| pos.layer == map.current_layer as u32 && vt.contains(&pos.pt))
        .map(|(e, _, pos)| (*e, DistanceAlg::Pythagoras.distance2d(player_pos, pos.pt)))
        .collect::<Vec<(Entity, f32)>>();

    targets.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
    let mut commands = legion::systems::CommandBuffer::new(ecs);
    let current_target = if targets.is_empty() {
        None
    } else {
        Some(targets[0].0)
    };
    commands.add_component(
        player_entity.unwrap(),
        Targeting {
            targets,
            current_target,
            index: 0,
        },
    );
    commands.flush(ecs);
}
}

This works by querying the ECS for possible targets - those with a Hostile and Position components. It then filters them to only include entities on the current map layer, and whose position is in the player's visible tiles list. It then maps the query, transforming it into a list of (Entity, Distance From Target) lines. These are then "collected" into a vector.

Next, we sort the vector by distance. We'd like to default to targeting the closest hostile. To avoid borrow-checker issues, it then uses a CommandBuffer to replace the player's Targeting component with a new one containing the new target list, current target and sets the index to 0. You'll see why we use the index in a moment.

Cycling Targets

Next, we move up to the player_turn function (still in player.rs). Let's add a new command to cycle targets:


#![allow(unused)]
fn main() {
pub fn player_turn(ctx: &mut BTerm, ecs: &mut World, map: &mut Map) -> NewState {
    render_tooltips(ctx, ecs, map);

    // Check for input
    let mut new_state = if let Some(key) = ctx.key {
        match key {
            VirtualKeyCode::Up | VirtualKeyCode::W => try_move(ecs, map, 0, -1),
            VirtualKeyCode::Down | VirtualKeyCode::S => try_move(ecs, map, 0, 1),
            VirtualKeyCode::Left | VirtualKeyCode::A => try_move(ecs, map, -1, 0),
            VirtualKeyCode::Right | VirtualKeyCode::D => try_move(ecs, map, 1, 0),
            VirtualKeyCode::T | VirtualKeyCode::Tab => cycle_target(ecs),
            _ => NewState::Wait,
        }
    } else {
        NewState::Wait
    };
    ...
}

We haven't written cycle_target yet. It's a new function, add it to the bottom of the player.rs file:


#![allow(unused)]
fn main() {
fn cycle_target(ecs: &mut World) -> NewState {
    let mut pq = <(&Player, &mut Targeting)>::query();
    pq.for_each_mut(ecs, |(_, targeting)| {
        if targeting.targets.is_empty() {
            targeting.current_target = None;
        } else {
            targeting.index += 1;
            if targeting.index > targeting.targets.len() - 1 {
                targeting.index = 0;
            }
            targeting.current_target = Some(targeting.targets[targeting.index].0);
        }
    });
    NewState::Wait
}
}

This function retrieves the player's Targeting component. If there are no targets, it sets current_target to None. Otherwise, it adds one to the index. If the index exceeds the length of the list, it resets back to zero. Then it sets the current target to the targets list entry at the index.

You can run the game now and see targeting information when you approach monsters.

Debugging the Targeting Data

To help prove to myself that the targeting was valid, I decided to draw a line between the player and their current target. This is a useful tip in general: sometimes decorating the map can make bugs jump out at you.

We returned a target point from the rendering information for this purpose. Let's modify tick in main.rs to send this information to the glyph rendering system:


#![allow(unused)]
fn main() {
impl GameState for State {
    fn tick(&mut self, ctx: &mut BTerm) {
        ctx.cls();
        render::render_ui_skeleton(ctx);
        let y = render::render_colonist_panel(ctx, &self.ecs, self.map.current_layer);
        let (y, target_pt) =
            render::render_targeting_panel(y, ctx, &self.ecs, self.map.current_layer);
        self.map.render(ctx);
        render::render_glyphs(ctx, &self.ecs, &self.map, target_pt);
}

You probably guessed that the next step is to open render/mod.rs and make use of the target_pt we are sending. Replace the function as follows:


#![allow(unused)]
fn main() {
pub fn render_glyphs(ctx: &mut BTerm, ecs: &World, map: &Map, target_pt: Option<Point>) {
    let mut player_point = Point::zero();
    let mut query = <(&Position, &Glyph)>::query();
    query.for_each(ecs, |(pos, glyph)| {
        if pos.layer == map.current_layer as u32 {
            let idx = map.get_current().point2d_to_index(pos.pt);
            if map.get_current().visible[idx] {
                ctx.set(
                    pos.pt.x + 1,
                    pos.pt.y + 1,
                    glyph.color.fg,
                    glyph.color.bg,
                    glyph.glyph,
                );
                if glyph.glyph == to_cp437('@') {
                    player_point = pos.pt;
                }
            }
        }
    });

    if let Some(pt) = target_pt {
        line2d_bresenham(player_point, pt)
            .iter()
            .skip(1)
            .for_each(|pt| {
                ctx.set_bg(pt.x + 1, pt.y + 1, GOLD);
            });
        ctx.set(pt.x, pt.y + 1, DARK_RED, BLACK, to_cp437('['));
        ctx.set(pt.x + 2, pt.y + 1, DARK_RED, BLACK, to_cp437(']'));
        ctx.set_bg(pt.x + 1, pt.y + 1, GOLD);
    }
}
}

This is essentially the same, but when we render the @ - we store the player position for future use. Then, if there is a target_pt - we plot a line from the player to the point. We skip the first tile (it will be the player) and set the background to be gold on each tile along the way.

Then we add a [ and ] around the target.

It turned out that my code worked. :-) I didn't really need the debug data - but I found it useful to prove to myself that it wasn't terrible!

Try it Out

Run the game now. You can move around, and when you encounter monsters they are targeted - with a line showing the current targeting path. Sometimes, you can see more than one monster at a time. Press T (or tab) and you can switch targets.

You can find the source code for hello_monsters here.

Next up

This concluded my second day of development. On the third day, we'll dive into shooting things, props, and starting to make staircases functional.