Let's code with the Roguelike tutorial - Part 2 - Entities and the map


Refactoring the off screen console code

As I mentioned in the last post, until I got a better handle on the off screen console code, I temporarily made all the Rust bindings call out to the default root console. Now that there's a bit of discussion on how the off screen consoles work, it's time to refactor the code to enable their use.

Instead of making the console a parameter, I created an object, and I made the console functions methods on that object. Also, in this case, I think it makes sense to use the static type system to help out here, and so a Root console and a normal Console console should be different types. One reason is that some of the console methods can only be called on root consoles. The other, possibly more important reason, is that once consoles are created, they are either root console or off screen consoles, and there's no switching back and forth between them depending on the state of the program - in other words, at compile time, I know which kind of console I'm using. This means the static type checker can do the verification.

In a more classical object oriented language, there would be an abstract base class, and Root and Offscreen would derive from some Console base class, with all the shared functionality. However, Rust doesn't support this kind of inheritance. Instead, it supports traits. There isn't really an analogue in Java or C++, so it's a bit hard to describe without using it. Essentially, it allows you to add on blocks of code to a class which add behavior - for example, you can add a trait that describes how to compare two instances of a class. Although this is only a rough analogy, I kind of think of them as a mixin (or like an Objective C category, if you're familiar with objc), with one important difference - they can be statically checked at compile time.

To set up the class structure, I created a Console trait, and I made two structs that implement this trait: a Root class (for root consoles) and an Offscreen class (for offscreen consoles). At first this felt a bit backwards, but after thinking about this some more and taking a look at this post (see the example with HWND at the end), I thought this was the right breakdown. The Console trait will own all the functions that can be used on all kinds of consoles, the Root class will contain functions that can only be used on the root, and the Offscreen class will contain functions that can only be used on offscreen consoles. Since libtcod uses the same functions for all consoles, the implementation is pretty easy. I just required a function that returns a pointer to its tcod peer (of type TCOD_console_t). For the root class, that's NULL, and for the offscreen console classes we save the return result TCOD_console_new. Then I passed that pointer along to the TCOD console code.

Since I made such a big change to the Rust API, I also moved all the console functions into their own submodule. If I'm going to be breaking the API, I might as well break it all at once so I only have to fix the calls once. I also added some using statements to help save on the typing.

This was a large change, but it was a straightforward change. It was mostly moving a bunch of functions into new files and directories.

I try to avoid refactoring and adding new functionality at the same time. So before adding the blitting code for off screen consoles, I did a quick test of the refactored console code. There was no difference, as expected.

The only wrinkle with the blitting code is the API. The libtcod code has this as a static function, where the source and destination are both parameters. In a sense, they are kind of peers - I don't see an obvious reason for destination.blit(src) or src.blit(destination). The closest is perhaps that you always end up mutating self, thus tipping it towards destination.blit(src). On the other hand, I don't know if that's a particularly common mental model, so I ended up just copying the libtcod API and making it a static function.

Creating player objects

Now it's time to generalize the player objects. The tutorial uses a generic Object class, which will contain all the parameters for anything that's representable on a screen.

When seeing this kind of design, a red flag immediately went up for me. From experience, I know this kind of design often leads to a diamond inheritance problem. In practice, I've found that inevitably, these different characteristics just don't break down well into mutually exclusive groups. As a very simple example, just consider having an Item that has derived classes Weapon and Armor. That seems straightforward enough until you have a spiked shield that can both defend and do damage.

Now in Python, due to its duck typing, I think this is probably fine. But in Rust, I suspect this is likely to lead to a problem later on where the class hierarchy becomes untenable. At first I thought Rust traits might also be a reasonable solution to this problem, but as far as I can tell, traits can't include ivars, only methods, so it's not clear that this is an obvious solution either. In the end, since all the current data is still relevant for all characters, I deferred this problem until later, when it actually becomes a problem. As I work through the game design and learn more about Rust, I should be able to make better decisions later.

The one thing I did change is the name - in anticipation of eventually creating something like an Entity-Component-System (ECS) architecture - I called my primary game object an Entity.

Changing the player to use this Entity class was mostly straightforward, although I did run into a few wrinkles with the Rust borrow checker that I'll discuss in the footnote at the end. I also changed my main loop to clear the entire screen and redraw everything each move, since I find that less error prone than undoing and redoing changes.

Making the map tiles

Conceptually, the map is a 2D grid of different floor tiles. There's lots of ways this could be represented, with many different trade offs, varying from a linked list of walls to a compressed bit field of tiles.

As always, for the first cut, I choose the simplest representation, even if I think there's a high chance I need to replace it later. In fact, in many cases, even if I know I'll need to replace it later, I'll still choose the simplest implementation first. I've had many situations where I've been sure about the design trade offs, but then the requirements shifted and made all that initial extra work wasted. But when I say simplest, I don't necessarily mean roll my own implementation. If I already have a library sitting around that I know how to use, I might get lucky and have both a fast and simple implementation out of the gate.

In this case, I rolled my own implementation, using the simplest representation I could think of: a 2D array of tiles. Now Rust arrays need to have sizes known at compile time, so instead of a Rust array, I used a Rust vector of vector of tiles. Each tile will have a set of attributes, and I just copied the attributes from the tutorial: blocked and block_sight. As long as I keep the code mostly abstracted, I should be able to easily change the implementation later if necessary.

For some reason, when working with 2D grids, I always get the (x,y) and the (y,x) coordinates confused. I think it has something to do with the grid rows running left and right, but the x coordinates actually select a column. I don't have that problem with general number systems, only grids, for whatever reason. In any case, I've started using (r,c) for (row,column), which I find a lot easier to reason about. Those coordinates do map to (y,x), which can occasionally caues some confusion, but I haven't found it to be a problem in general.

Also, to be able to store the tiles in a vector, I needed to make the Tile class cloneable. There's no real reason to make the map own references to tiles, since the map is going to be the object that owns the tiles, so this solution suffices.

The last thing to do is draw the map. The first version of my drawing code accesses the tiles vector of vectors directly, instead of having it come through a function. Isn't this a violation of encapsulation?

Yes, it is. Does that make it bad code?

In this case, I don't think it's the worst thing. There are a few mitigating circumstances:

So while I think it's ok to go ahead and make this into its own class right now, neither do I think it's necessary. In reality, I'm almost certain that I'll be adding more functionality to the map, which means I will have to refactor it into a better interface. But at that time, I'll know what features I plan to add, which means I'll have a better idea of what the interface should look like. If I refactor now, I'll have to guess.

One other thing I noticed is that the drawing code has two parts: one part to draw the characters, and one part to draw the tiles. Both of them have (x,y) positions. Couldn't I unify this code to make it cleaner by making the map tiles entities and treating these uniformly? Also, this violates that cherished rule "don't repeat yourself (DRY)."

Yes, I potentially could. I don't think it's the right time yet though. It's not obvious that the map and the entities will evolve in the same way as the game design continues. I might do a lot of work now to have to undo it later when I find out that the map implementation actually has to be completely rewritten for some game feature. And having the draw function first draw characters, then draw tiles, just isn't that hard to read. So I left it the way it is now, but could consider unifying the implementations at a later time.

Lastly, to handle collisions, I also have to pass the map into handle_keys. I should probably combine the player and the map together into a single game state object, but that's a task for another time. (Translation: I don't have the energy to do it now).

Summary

After the big mess with wrestling with the Rust FFI, things are going more smoothly now. My biggest struggle with the language is now dealing with object ownership and references. That's a bit expected since that's one of the major things that's different in Rust, so hopefully it will get a bit better as I gain more experience.

Rust object ownership

My first attempt at updating the main loop looked like this:


let mut player = Entity { x: SCREEN_WIDTH / 2, y: SCREEN_HEIGHT / 2, ch: '@', color: Color::white() };
let mut npc = Entity { x: SCREEN_WIDTH / 2 - 5, y: SCREEN_HEIGHT / 2, ch '@', color: Color::yellow() };
let entities = [player, npc];

// ... code that sets up console

while !console::console_is_window_closed() {
    // ... code that iterates through entities and draws on console and blits

    let key = console::console_wait_for_keypress(false);
    if handle_keys(key, &mut player) {
        break;
    }
}

This code snippet creates a player and npc, and puts them into an entities array. Then in the main loop, it iterates through the entities and draws it (the exact omitted for brevity) and then it handles keypresses.

Although this code looks natural enough, it doesn't compile. The compiler complains about a "use of moved value." Specifically, the 'player' value was moved during the creation of the entities array, and it was used at the call to handle_keys.

Although the error is straightforward enough, this took me a while to really wrap my mind around, and it has to do with the Rust reference checker. When the player entity is first created, the reference to that object is stored in the player variable. Since player isn't copyable, when I subsequently create the entities array, the player reference is moved into the array, meaning, the original reference is no longer valid. Therefore when I go and try to use the player reference again, the compiler complains.

This can be fixed by taking a new borrow reference to the player object in the entities array. So just before handle_keys, I can write


    let mut player = &mut entities[0];

This borrows a mutable reference to the first object in entities and is how we get access to the player again.

Theoretically, I could have had the entities array be an array of borrowed references to the player and npc, instead of the objects themselves. However, in this case, I don't think that makes sense. I actually do want the owner of the entities to be the entities array. The player and npc variables are just temporary storage for readability in the code. On the other hand, during the handle_keys, I don't want to actually own the player object - I just want to use it for a bit to handle the keypress, and then let go of the reference.

When testing a mental model (in this case, the borrow model), I usually try some experiments to see if they work as expected. In this case, I tried adding


player.x = 0;

right after the creation of the entities array. Oddly enough, that compiled. This really confused me for a while, until I realized that statement is technically unnecessary, since I never use that player object again (I had already changed the main loop to reacquire player from the entities array). So instead I changed the test code to do a print on player.x instead, and that failed to compile as expected.

My best guess is that dead code doesn't trigger any Rust warnings. Either that or something else is still missing from my understanding of borrow references...


In this series: Contents, previous, next


by Yosen
I'm a software engineer who works on both mobile and web programming. I think STEM education is important. I also follow pop culture and dabble in music and the digital arts.