Roguelike Tutorial in Rust: Part 5
This is Part 5 in a many part series on how to make a roguelike game in Rust. If you’re lost, check out the Table of Contents to figure out where you should go.
Combat! Part III #
Last time we made a ton of progress on combat. We’ve got a monster on the screen; we can issue combat instructions; we’ve got different parts of the screens reserved for different types of output. Things are looking pretty okay. Let’s keep going!
Refactoring #
While I was working on Part 4 I saw a guide go into pull request on the Rust GitHub. The guide was The Rust Crates and Modules Guide and it was super duper exiting. I’d been really curious about how to do this The Right Way™ for awhile, and I was getting tired of this in my tab bar:
It was getting confusing!
After reading the guide I learned that modules don’t need to live in a folder (like src/game/mod.rs
), they can just be named after the module! So I changed a lot of names, but didn’t change any code. It was a pretty big refactor but it’s so good. You can see the whole thing in this commit. I suggest you take the time to go for it.
When I did this refactor I split rending into two separate modules, renderers
and windows
. That meant changing a bunch of my imports around. After I did that, I ran into this really weird error:
Compiling dwemthys v0.0.1 (file:///Users/jmcfarland/code/rust/dwemthys)
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:14:32: 14:61 error: source trait is private
/Users/jmcfarland/code/rust/dwemthys/src/main.rs:14 let mut b = Actor::heroine(game.windows.map.get_bounds());
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I spent some time Googling it but couldn’t pin it down. I even spent some time on play.rust-lang.org to try and create a repro to show the folks in the Mozilla #rust channel, but I couldn’t reproduce it. So I added a function to Windows
, called #get_map_bounds
.
pub fn get_map_bounds(&self) -> Bound {
self.map.get_bounds()
}
That got rid of the error. If anyone reading this knows what was up, feel free to send me an email, I’d love to know.
Maps #
One of the reasons we didn’t get to implement combat in the last part was because we didn’t have a good way to tell whether or not there was an enemy in the square the player was attacking. We actually didn’t even have that great of a way to tell what square the player was attacking. We could use the players position to figure out which square they were attacking, but to find out if there was anything in that square we’d have to iterate our NPCs array and look for an actor in that position. That’s not so great.
Implementing a map system should help us fix that. Maps are a pretty big change. A lot of code moved when I went through this exploration. The main motivator is to have a query interface to ask the Maps
object whether or not there’s an enemy on a square. In order to do that we’ll move over all of the logic around iterating our vector of Actor
objects to the Maps
struct. This means that GameState#update
will just call Maps#update
. In order to preserve the order in which things are rendered we’ll introduce the concept of “layers”. Our Maps
object will be made up of several attributes, and each of those will be a Map
. Those are:
- terrain
- enemies
- friends
- PCs
We haven’t implemented terrain yet, but we will in a future tutorial.
Let’s get started.
Maps Struct #
Let’s start with the more simple of the two new structs we’ll be introducing Maps
.
pub struct Maps<'a> {
pub terrain: Box<Map<'a>>,
pub enemies: Box<Map<'a>>,
pub friends: Box<Map<'a>>,
pub pcs: Box<Map<'a>>
}
Maps
is very similar to our Windows
struct, it’s just a container for all the different maps we have. It will allow us to address a map by name if we need to, or all of them at once. We need the lifetime value 'a
because Map
requires it. You’ll see why in a minute.
The implementation of Maps
is pretty straight forward:
impl<'a> Maps<'a> {
pub fn new(size: Bound) -> Maps<'a> {
let terrain = box Map::new(size);
let enemies = box Map::new(size);
let friends = box Map::new(size);
let pcs = box Map::new(size);
Maps {
friends: friends,
enemies: enemies,
terrain: terrain,
pcs: pcs
}
}
pub fn update(&mut self, windows: &mut Windows) {
self.pcs.update(windows);
self.terrain.update(windows);
self.friends.update(windows);
self.enemies.update(windows);
}
pub fn render(&mut self, renderer: &mut Box<RenderingComponent>) {
self.terrain.render(renderer);
self.friends.render(renderer);
self.enemies.render(renderer);
self.pcs.render(renderer);
}
pub fn enemy_at(&self, point: Point) -> Option<&Box<Actor>> {
let enemies_at_point = &self.enemies.content[point.x as uint][point.y as uint];
if enemies_at_point.len() > 0 {
Some(&enemies_at_point[0])
} else {
None
}
}
}
First, we have our ::new
method which you should be pretty familiar with by now. We pass it a single argument, size: Bound
which we use to determine how big the map needs to be.
Our #update
and #render
methods are very simple, they just forward the command to each of the maps.
#enemy_at
takes a single argument, the point
and looks to see if the enemies map contains anything at that position. Right now it just returns a single enemy, assuming that the first enemy at that point is the one we want. We’ll address that later. Notice that #enemy_at
returns an Option
instead of just the enemy. This way we can do pattern matching and properly program the “no enemy at location” scenario.
The implementation of #enemy_at
clues you in a bit to how Map
will be implemented. Map
will store a nested set of Vec
objects. It will have one for the x-axis, one for the y-axis and then one for all the actors in that location.
Map Struct #
So we have Maps
all setup, we just need a Map
object to store in it. Let’s look at the struct:
pub struct Map<'a> {
pub content: Vec<Vec<Vec<Box<Actor<'a>>>>>,
pub size: Bound
}
The content
field stores all of our actors. If you think about it, you’ll have a bunch of arrays like this when it’s initialized:
[[[], [], []],
[[], [], []],
[[], [], []]]
Which loosely resemble the grid of our game. When we want to add an actor to {1, 2}
we need to grab the second array, and then the first array inside that, ie contents[2][1]
.
Let’s go over the implementation:
impl<'a> Map<'a> {
pub fn new(size: Bound) -> Map<'a> {
let content = Map::init_contents(size);
Map {
content: content,
size: size
}
}
pub fn init_contents(size: Bound) -> Vec<Vec<Vec<Box<Actor<'a>>>>> {
let mut contents : Vec<Vec<Vec<Box<Actor>>>> = vec![];
for _ in range(0, size.max.x) {
let mut x_vec : Vec<Vec<Box<Actor>>> = vec![];
for _ in range(0, size.max.y) {
let y_vec : Vec<Box<Actor>> = vec![];
x_vec.push(y_vec);
}
contents.push(x_vec);
}
return contents;
}
pub fn push_actor(&mut self, point: Point, actor: Box<Actor>) {
self.content.get_mut(point.x as uint).get_mut(point.y as uint).push(actor);
}
pub fn update(&mut self, windows: &mut Windows) {
let mut new_content = Map::init_contents(self.size);
for x_iter in self.content.iter_mut() {
for y_iter in x_iter.iter_mut() {
for actor in y_iter.iter_mut() {
actor.update(windows);
if actor.is_pc {
Game::set_character_point(actor.position);
}
let point = actor.position;
let new_actor = actor.clone();
new_content.get_mut(point.x as uint).get_mut(point.y as uint).push(new_actor);
}
}
}
self.content = new_content;
}
pub fn render(&mut self, renderer: &mut Box<RenderingComponent>) {
for (x, x_iter) in self.content.iter_mut().enumerate() {
for (y, y_iter) in x_iter.iter_mut().enumerate() {
for actor in y_iter.iter_mut() {
let point = Point::new(x as i32, y as i32);
renderer.render_object(point, actor.display_char);
}
}
}
}
}
First, let’s go over ::new
. Notice it takes the same size
param that Maps::new
took. It makes a call out to init_contents
to create the nested Vec
s and then it creates and returns the Map
object. ::new
takes a lifetime parameter 'a
and uses it for creating the Box<Actor<'a>>
.
We use ::init_contents
to create the nested vector. Originally I did this because it was fairly messy logic and I wanted to contain it in its own method, but it came in handy later on. It’s fairly straightforward though. It iterates a given number of times in nested loops to create the appropriately sized arrays. This is so that we can access contents[1][2]
without having to first check and see if it exists.
#push_actor
is a helper method to add an actor to the appropriate Vec
. I didn’t want to expose the implementation details of Maps
so this seemed like the best choice.
#update
is where we’ll put the logic to update all the actors on the map. Before we just iterated our NPC array and then called update on the heroine. Now we’re delegating that to Maps
and Map
. It loops through it’s nested structure and calls update on each actor. Notice that it doesn’t stop there. It does some weird stuff.
First it checks is_pc
on the Actor
(a field we haven’t added yet) and if so it updates the CHARACTER_POINT
constant in Game
. That’s because we removed the concept of the character from the main game loop.
Next it calls Actor#clone
on the actor. Another method that doesn’t exist yet. It uses that to create a new Actor
object and then it stores it in the new vector we create at the beginning of the method. After doing this for all the actors, it replaces Map#content
with new_content
. We do this to avoid complicated ownership issues when trying to update content
directly in the for loops. Because we borrow self
at the beginning of the loops we can’t modify self
from within the for loops.
Next we have #render
. #render
takes in a RenderingComponent
and uses that to draw the characters on screen based on their location. Notice that it uses the location from the map and not the one stored on the character itself. We use the map as the authoritative source of location information. The location stored on the Actor
is just for convenience. (This will probably bite us in the ass later.)
You’ll have to import a bunch of stuff to get this to compile, but you can use the compiler to figure that part out [=
Actor #
Let’s go over the changes to actor mentioned above. First we need the is_pc
field:
pub struct Actor<'a> {
pub id: uint,
pub position: Point,
pub display_char: char,
pub movement_component: Box<MovementComponent + 'a>,
pub is_pc: bool
}
Pretty simple actually, just add a new bool
. Next we need to modify all our constructors (::heroine
, ::cat
, ::dog
, ::kobold
) to use this field. All of them should be false
except ::heroine
. I’ll leave that as an exercise for you.
Next we need to implement the #clone
method. Rust is pretty smart about some things. If you call #clone
on an object it assumes you mean to use the core::clone::Clone
trait, so when we go to implement Actor#clone
we’ll do it by implementing the Clone
trait:
impl<'a> Clone for Actor<'a> {
fn clone(&self) -> Actor<'a> {
let mc = self.movement_component.box_clone();
Actor::new(self.position.x, self.position.y, self.display_char, mc, self.is_pc)
}
}
It’s pretty easy. We just create a new Actor
with all the same fields as the current one. The only weird thing is calling #box_clone
on movement_component
. I couldn’t get the regular Clone
trait there to work so I asked in the Rust IRC channel and they helped me out. Apparently you can’t define a method that has both &self
as a variable and Self
as a return type.
That’s it for Actor
, let’s talk about MovementComponent
next.
MovementComponent #
The only real change to MovementComponent
we need to make is to add the #box_clone
method. First we’ll add its signature to the MovementComponent
trait:
pub trait MovementComponent {
fn new(Bound) -> Self;
fn update(&self, Point, &mut Windows) -> Point;
fn box_clone(&self) -> Box<MovementComponent>;
}
Then we’ll add the implementation to each of our movement components. Here it is for UserMovementComponent
:
fn box_clone(&self) -> Box<MovementComponent> {
box UserMovementComponent { window_bounds: self.window_bounds }
}
I’ll leave the rest of the implementations up to you. (Maybe we’ll turn this into a macro someday?)
Once we’ve done this we’ll have made all the changes we need to get Maps
and Map
to work. Next we need to integrate them!
Game #
First thing to do is add Maps
as a field to Game
:
pub struct Game<'a, 'b> {
pub exit: bool,
pub window_bounds: Bound,
pub rendering_component: Box<RenderingComponent + 'a>,
pub game_state: Box<GameState + 'a>,
pub windows: Windows<'a>,
pub maps: Maps<'b>
}
Notice that we also added a new lifetime parameter to Game
. That’s because both Windows
and Maps
need their own. I’m not sure why Windows
and the Box
‘s can share though.
We had to update the method signature of Game::new
to match (as well as the imple
statement for Game
).
pub fn new() -> Game<'a, 'b> {
// init all the bounds
// init our windows and rendering component
// init our game state
let maps = Maps::new(map_bounds);
Game {
exit: false,
window_bounds: total_bounds,
rendering_component: rc,
windows: windows,
game_state: gs,
maps: maps
}
}
Sweet! Now we have a maps
field on Game
. Let’s start changing some shit! First thing we’ll do is change Game#render
and GameState#render
. They’ll no longer take a vector of npcs or the character as arguments. Game#render
turns into:
pub fn render(&mut self) {
self.game_state.render(&mut self.rendering_component, &mut self.maps, &mut self.windows);
}
And GameState#render
turns into:
fn render(&mut self, renderer: &mut Box<RenderingComponent>, maps: &mut Maps, windows: &mut Windows) {
renderer.before_render_new_frame();
let mut all_windows = windows.all_windows();
for window in all_windows.iter_mut() {
renderer.attach_window(*window);
}
maps.render(renderer);
renderer.after_render_new_frame();
}
We got rid of the iteration of the npcs
object and the call to character.render
and just call #render
on maps
instead.
Next we’ll do the same thing to Game#update
and GameState#update
. He’s Game#update
:
pub fn update(&'a mut self) {
if self.game_state.should_update_state() {
self.game_state.exit();
self.update_state();
self.game_state.enter(&mut self.windows);
}
self.game_state.update(&mut self.maps, &mut self.windows);
}
Same story. Get rid of npcs
and character
and just use maps
. GameState#update
becomes:
fn update(&mut self, maps: &mut Maps, windows: &mut Windows);
We change the method signature to reflect what we’re passing the individual components. Here’s how that plays out in MovementGameState#update
:
fn update(&mut self, maps: &mut Maps, windows: &mut Windows) {
match Game::get_last_keypress() {
Some(ks) => {
match ks.key {
// Because Shift is used for attack keys we don't want to do
// anything when it's pushed. We can check for shift when we
// process the next keypress
SpecialKey(KeyCode::Shift) => {},
_ => {
maps.update(windows);
}
}
},
_ => {}
}
}
If we get a movement key we just call #update
on maps
. The change to AttackInputGameState#update
is a bit more involved:
fn update(&mut self, maps: &mut Maps, windows: &mut Windows) {
match Game::get_last_keypress() {
Some(ks) => {
let mut msg = "You attack ".to_string();
let mut point = Game::get_character_point();
match ks.key {
SpecialKey(KeyCode::Up) => {
point = point.offset_y(-1);
msg.push_str("up");
self.should_update_state = true;
},
SpecialKey(KeyCode::Down) => {
point = point.offset_y(1);
msg.push_str("down");
self.should_update_state = true;
},
SpecialKey(KeyCode::Left) => {
point = point.offset_x(-1);
msg.push_str("left");
self.should_update_state = true;
},
SpecialKey(KeyCode::Right) => {
point = point.offset_x(1);
msg.push_str("right");
self.should_update_state = true;
},
_ => {}
}
if self.should_update_state {
match maps.enemy_at(point) {
Some(_) => {
msg.push_str(" with your ");
msg.push_str(self.weapon.as_slice());
msg.push_str("!");
windows.messages.buffer_message(msg.as_slice());
},
None => {
windows.messages.buffer_message("No enemy in that direction!");
}
}
}
},
_ => {}
}
}
First we updated the methods signature to match the new signature. Then we update the match
statement to track a point. The point tracked represents where the user is attacking. Then we call #enemy_at
with the point being tracked to see if there’s an enemy at in that square. If there is we use the same string building logic used before. If not we buffer a different message that says there’s no enemy in that square. Cool!
Main #
Since we’ve changed Game
and how we’re storing our actors, we need to update our main game loop.
First we’ll update how we’re creating and registering our actors:
let mut game = Game::new();
game.maps.friends.push_actor(Point::new(10, 10), box Actor::dog(10, 10, game.windows.get_map_bounds()));
game.maps.friends.push_actor(Point::new(40, 25), box Actor::cat(40, 25, game.windows.get_map_bounds()));
game.maps.enemies.push_actor(Point::new(20, 20), box Actor::kobold(20, 20, game.windows.get_map_bounds()));
game.maps.pcs.push_actor(Game::get_character_point(), box Actor::heroine(game.windows.get_map_bounds()));
This is super janky but it works for now.
Next we need to change all of our calls to Game#render
and Game#update
to not use any argument:
game.render()
while !(Console::window_closed() || game.exit) {
// game loop
game.update();
game.render();
}
Movement Component #
Now we need to implement our #box_clone
method for MovementComponent
and all the different components we have.
First we’ll add the method signature to our trait:
fn box_clone(&self) -> Box<MovementComponent>;
Then we’ll add an implementation to our components:
fn box_clone(&self) -> Box<MovementComponent> {
box AggroMovementComponent { window_bounds: self.window_bounds }
}
I’ll let you add the rest of the methods.
That’s that! We now have a map system for our game.
Removing Globals #
One of the coolest parts about writing this tutorial is getting to share cool new things with lots of people. Because I’m learning Rust just fast enough to implement the next part of the tutorial, I don’t always do things The Rust Way™. Fortunately awesome people reading this reach out to me and explain a better way to do it. Recently GitHub use GBGamer sent me an email explaining a way to not use globals and linked me to their version of the game. Instead of using global vars they suggested using the RefCell
object in conjunction with the Rc
object.
I hadn’t heard of either of these, so I looked it up. My understanding (please correct me if you’re reading this and I’m wrong) is that the Rc
object is a reference-counted, shared pointer and it’s immutable. Instead of Rust deciding at compile time when something will be freed, it’s decided at run time. As soon as the last reference to it is gone, it’s freed. You can read more about it at the Rust Rc Docs.
The RefCell
object is a type that is allowed to be mutated through shared pointers (&T
vs &mut T
). RefCell
requires you to get a write-lock on it before writing to it. This is done by calling #borrow
on it. You can read more about RefCell
(and it’s sibling Cell
) on the Rust docs.
So what does that mean for us? It means we can take all of our information that was global before, put it in a struct, put that struct in a RefCell
and that RefCell
into an Rc
and pass around copies of it to anything that will need it. Let’s see how that’s done.
MoveInfo #
First we’ll create a new struct called MoveInfo
which will hold all of our global movement based information:
pub struct MoveInfo {
pub last_keypress: Option<KeyboardInput>,
pub char_location: Point,
pub bounds: Bound
}
impl MoveInfo {
pub fn new(bound: Bound) -> MoveInfo {
MoveInfo {
last_keypress: None,
char_location: Point::new(40, 25),
bounds: bound
}
}
}
It’s very straightforward. It takes all the information we used to store globally and moves it into the struct. I moved the map bounds into the struct as well because it was accessed in so many places.
Using MoveInfo #
This is a pretty far-reaching change. First off, any file that’s using Rc
and RefCell
need these two import lines:
use std::cell::RefCell;
use std::rc::Rc;
Second, we need to add a move_info
as an attribute on Game
:
pub struct Game<'a, 'b> {
// stuff
move_info: Rc<RefCell<MoveInfo>>
}
Then we need to update our Game::new
method to create the MoveInfo
object:
pub fn new() -> Game<'a, 'b> {
// create the bounds and states
let move_info = Rc::new(RefCell::new(MoveInfo::new(map_bounds)));
let maps = Maps::new(move_info.clone());
Game {
exit: false,
window_bounds: total_bounds,
rendering_component: rc,
windows: windows,
game_state: gs,
maps: maps,
move_info: move_info
}
}
Notice we’re passing move_info.clone()
to Maps::new
instead of the map_bounds
we were passing before. Keep in mind that move_info
is not a MoveInfo
object but a Rc<RefCell<MoveInfo>>
object, so when call #clone
on it, we’re calling Rc#clone
, not MoveInfo#clone
. We’re not cloning the data structure, but the reference-counted reference to it. We’ll do this every time we pass move_info
to something.
Next we need to find all instances of Game::set_last_keypress
, Game::get_last_keypress
, Game::get_character_point
and Game::set_character_point
and replace them with calls into MoveInfo
. This also means passing MoveInfo
to all movement components, game states, actors and maps. You can see my commit that introduced this change here. I won’t walk through each one because it’s pretty repetitive, but I will call out a couple things.
Scoping #
The first is “scoping” (is that the right word?). Access the fields in move_info
is not as simple as:
move_info.last_keypress
That’s because Rc
doesn’t have a field named last_keypress
. Instead we have to jump through a number of hoops. First we have to call #borrow
on the Rc
. This gives us an immutable pointer to the RefCell
, which we then have to dereference with #deref
which gets us a pointer to the MoveInfo
object.
move_info.borrow().deref().last_keypress
The unfortunate part is that we need a way to tell the compiler when to release the “borrow” (I kind of think of them as locks). If you attempt to borrow something that is already borrowed your program will crash and burn with an error like this:
task '<main>' failed at 'RefCell<T> already borrowed', /build/rust-git/src/rust/src/libcore/cell.rs:306
That’s a runtime failure, and not a very descriptive one. How do we keep this from happening? We create new scopes for all of our borrows, and when the scopes end the borrow is released:
{
move_info.borrow().deref().last_keypress
}
If we need to store a value from MoveInfo
in a variable we do so like this:
let last_keypress = {
move_info.borrow().deref().last_keypress
};
Then we can reference last_keypress
later in the method. Setting values is fairly straight forward, but you need to use the _mut()
versions of the methods:
{
move_info.borrow_mut().deref_mut().last_keypress = keypress;
}
Initialization #
The last thing I want to mention is the game initialization in src/main.rs
, specifically the Actor
initialization. It’s not too complicated, but can cause headaches. Here’s mine:
game.maps.friends.push_actor(Point::new(10, 10), box Actor::dog(10, 10, game.move_info.clone()));
game.maps.friends.push_actor(Point::new(40, 25), box Actor::cat(40, 25, game.move_info.clone()));
game.maps.enemies.push_actor(Point::new(20, 20), box Actor::kobold(20, 20, game.move_info.clone()));
let char_location = {
game.move_info.borrow().deref().char_location
};
game.maps.pcs.push_actor(char_location, box Actor::heroine(game.move_info.clone()));
The big thing to note is that we no longer pass around the bounds (which has a cascading effect). We pass around the Rc<RefCell<MoveInfo>>
which means we need to call #clone
on it when passing it to new objects that will be storing the reference.
Finishing up #
You should be able to use my commit and the notes above to figure out how to implement the rest of this refactor. At the end you get to do my favorite thing, delete some code. Get rid of the constants LAST_KEYPRESS
and CHAR_LOCATION
in src/game.rs
and the static methods on Game
.
Lifetimes #
About three weeks passed between when I started this article and when I finished it. That’s a long time in Rust land. When I came back to the project, it didn’t compile anymore. I was having a lot of lifetime issues. I made a series of changes which mostly removed named lifetimes and replaced them with 'static
lifetimes. You can see the sum changes in this commit range.
In that time tcod-rs also underwent some breaking changes, so in that commit range you’ll see some related changes.
Conclusion #
I think this is a good place to stop for this part. We just took a huge step towards getting combat working. Next time we’ll look at dealing and receiving damage. Here’s the GitHub tag for v5.0.