Let's code with the Roguelike tutorial - Part 8 - Items and Inventory

Adding item and inventory support

With the implementation of the ECS architecture, it should be a bit easier to add more features.

For this next set of features (items and inventory), I broke it into the following subtasks:

None of these seem too complicated from a design point of view.

Creating items

I needed to add items that would be displayed on the map, that the player could pick up. With a component system, this is straightforward. Just as all objects that can engage in combat need a fighter component with HP and other stats, all items that can be picked up will have an item component. Eventually this component will have attributes for the different possible item effects, but since I only had one effect to start, the component didn't need any ivars for now.

Placing items on the map is very similar to placing monsters on the map. There's probably some value in refactoring these to try to use the same loop here - something to consider if we continue to add more entities onto the map.

I also needed to add a render order for the items. I drew it above corpses, but below characters.

And that was all the changes I needed to add in item support on the map! I tested it, and while I can't pick them up yet, I can see that they're being created and I can walk over them.

Adding an inventory system

The next step is to create an inventory system to track the items held by the player character.

For this, I added a simple vector of items to the Game state. However, I created a new type InventoryItem to track these items, instead of just storing an Entity. Why did I do this? It prevents bugs where an item might get added to the inventory, but isn't actually usable. The InventoryItem only contains the data necessary to actually use and display the item, and it doesn't use an optional component, so there's no run time check to see if it's really a usable item.

I then added a function to remove an item from the entities list and convert it to an InventoryItem and add it to the inventory array. Since I needed to remove the entity, I passed in the array index (as opposed to a simple reference to the entity, like I use in many other places). The pick_up function assumes and asserts that the entity has an ItemComponent, but can still fail if the inventory is full, so it returns a boolean indicating success or failure. Side note, I think Rust has a Result type which has an enum for "success" and "fail with error," which I may look into and use in the future.

Finally, I called pick_up when the player presses the 'g' key. It iterates through the enemies array, finds the item that's currently on the player location, if any, and passes that index to pick_up. Both the inventory code and the player movement code borrow from some of the game state, and the borrow checker complained. I probably could have scoped it to get it working, but I extracted it out into different functions since those operations were fairly independent.

I noticed that there's a lot of places where I need to get the current position of the player character. It's probably worth looking into making that a common function at some point.

And yes, I know that the enemies array isn't really just enemies anymore, it's now enemies and items. I'll fix that at some point.

Adding an inventory menu

Next, to add an interface to use the items. When the player presses 'i', I should display a list of current inventory items, and allow the user to select one from the menu.

First I considered how to manage the game state. When the inventory menu is up, the player shouldn't be able to move around, or do any other actions, until the menu is dismissed. So I have to keep track of the game state. There's two ways to do this.

One is to have a bunch of little game loops that get called for the different states the game is in. So for example, in the main game loop, when I press 'i', I draw the menu, then run a separate loop that calls the tcod library to wait for the next keypress. When an item is used, or the player chooses not to use an item, I drop the menu, then go back to the outer game loop that handles character movement.

The other method is to have one big game loop which combines all the possible states together. In this method, the main game loop would have a state called "inventory." While in that state, the rendering function would put up the inventory menu. Then, the game loop switches on the current state, and if it's in the "playerturn" state, it handles the arrow keys as moving the player. If in the "inventory" state, it ignores the movement keys and handles the inventory usage.

I tend to prefer the big game loop method. Although, at first glance, it leads to a bunch of switch statements and all the state management crammed into one function, I find that this often makes it easier to work out how a key press impacts the UI. As a whimsical example, old games used to have the notion of a boss key. Implementing this in the unified game loop is straightforward - add an extra key handler to each state. In the distributed game loops, it's difficult to know if this functionality has been properly added everywhere. To take a more serious example, you may want to add configuration options that can be used in every game state - things like turning on or off the sound, or disabling or hiding a multiplayer chat log. In that case, it's easier to make sure this works properly from every user state.

To test the state management, I didn't need any of the other inventory code working. I simply drew an empty window (which will eventually contain the inventory items) to an offscreen console and blitted that to the screen, and checked to see that 'i' and escape and can get in and out of the window. It worked as expected.

Once I got the state management working, I could populate the window with the inventory items. To keep the windowing code mostly generic, I passed in a slice of strings to print as the items. Then I can take my inventory items, convert that to a vector of strings, and pass that in to draw the window.

This turned out to be quite a bit harder than it seemed.

The first thing I wanted to do was generate a list of item names from the list of items. I could have done this directly by iterating through the list, and manually getting the name and continually appending it to a list. But semantically, this is actually a higher level construct: Take a list, and transform each item in the list with some operation, producing a new list of the same length. In functional languages this is typically referred to as a "map" operation (Python calls them "list comprehensions"). I tried to use it here, but it turns out, the item names are actually String objects, and so can't be moved out of the InventoryItem without the borrow checker complaining.

There might have been some way to use an iterator with references, and create a list of references, but I don't know how to tell the compiler that this result will have a lifetime that is definitely shorter than the inventory array.

I ended up having to clone each string, and returning the clone from the map operation. Hopefully Strings have a copy on write optimization or something to keep this from becoming too expensive.

Now I had a vector of String, and I can pass a reference to that vector to my render_menu function. Fortunately that works as expected.

However, this didn't look very good when the inventory is empty, since it just displays a blank window. So instead of passing in an empty list, I passed in a vector of one string ["No inventory items"]. (It's debatable whether this is a particularly good UI, but I just followed the original Python tutorial in this regard).


let options = if game.inventory.len() == 0 {
  vec!["No inventory items."]
} else {
  game.inventory.iter().map(|x| x.name.clone()).collect()
}
RenderSystem::render_menu(game, "Press the key next to an item to use it, or esc to cancel.\n", &options, INVENTORY_WIDTH);

This didn't compile, failing with some type error.

So, I had forgotten that in Rust, a literal string in double quotes and an object of type String, can't always be automagically converted back and forth. But this seemed like a common enough operation that there should be a way to write functions that work with both. Sure enough, some searching on the web leads to this post and I am able to use this function declaration:


fn render_menu<T: AsRef<str>>(game: &mut Game, header: &str, options: &[T], width: i32) {
  // ...
}

This syntax looks kind of threatening, but breaking down the pieces is actually relatively straightforward.

This implements render_menu with generics; so the list of options that are passed in aren't a slice of literals (&str), or a slice of Strings, but a slice of type T. The AsRef is a bound on T, meaning T can't be any generic type, but must be a type that implements the trait AsRef. Then in the function, I can call as_ref on each item in the options, and for both &str and String I'll be able to use it as a literal.

Alright, with that done, I tested it again and... I got the same error.

What?

This took a long time to debug, and in the end turned out to be the simplest thing. Sometimes bugs are like that.

The real problem was that the main part of the if clause returns a Vec<&str>, but the else clause returns a Vec. Those two types are not the same. Once I figured that out I was able to easily fix it by using String::from("No inventory items.") instead of the string literal. So I didn't have to mess with any of that AsRef stuff after all. Well, I already implemented it, and it might be useful at some point, so I just left it alone.

Using an item

Most of the heavy lifting is now done. To complete the final step and actually use the item, I made some small changes to the game loop. When the player is in the inventory state, check to see if the player presses a selection that maps to a valid item. If so, convert the selection to an index and call the appropriate use function in ItemSystem. I chose to keep the player in the inventory state so they could use another item, but I could have also bounced the player out of the inventory state, and press 'i' again to go back to the inventory. It just depends on what kind of UI I wanted to have.

I did have to scrub a few bugs with the state manager. Since many of the states allow the player to press escape to back up to the previous state, I tried to gather those together to reduce the code. But that actually turned out to make the state manager harder to verify - which I should have known from experience but for some reason thought I could do better this time. My recommendation with state machines is: always keep the code for the different states separate, even if you copy and paste the code. It's much easier to verify the states when all the paths are independent, instead of trying to work out all the different paths through the code; and I'd rather risk forgetting to fix a few copied and pasted versions, than to try to work out all the possible logic paths through a complex state machine. It can also be useful to draw the state machine separately, manually verify all the transitions, and attach that separately as documentation.

Summary

Much of the overall architecture is in place, so the amount of code design that's needed for new features is steadily decreasing, although it can take a bit more code to implement because I had to add different elements to the game engine, components, and systems. In this case, I think the flexibility is a worthwhile benefit for the additional infrastructure.

The other gotcha was spending a lot of time because I treated string literals and string objects as equivalent. In many other languages there's a lot of auto conversions and aliasing for these two types. I'll have to remember that in Rust these two types need to be treated differently.


In this series: Contents, previous, next


by Yosen
I'm a software developer for both mobile and web programming. Also: tech consultant for fiction, writer, pop culture aficionado, STEM education, music and digital arts.