Let's code with the Roguelike tutorial - Part 1 - Drawing the player and moving around


Drawing the first screen

First, I took a look at the sample code to get an idea of the design.

I saw that it makes some libtcod calls, then goes into a while loop and makes some more calls to update the screen each time through the loop.

From just these dozen lines, I'm already able to infer quite a bit about how the library is designed:

Ok, I started with making the first calls. I think the function that actually kicks off all the initialization is console_init_root. I did notice, though, that it's not the first libtcod call in the program. There's another one to set the custom font, that's called before init.

Is this call required before calling init? Will there be an intelligent fallback if I don't set the custom font? I don't know.

I could try to figure it out by browsing the documentation; or maybe I can dig into the source code if I'm so inclined; or I can just try it out in the Python interpreter.

In this case, I didn't bother with any of those. I just tried it out to see what happens. It might work, and if it crashes, then I can decide how I want to investigate.

Since C is generally much easier to interface with, I set up my FFI calls to interface with TCOD_console_init_root. From the documentation, it's defined as:


void TCOD_console_init_root(int w, int h, const char * title, bool fullscreen, TCOD_renderer_t renderer)

which can be translated to the equivalent Rust FFI as:

fn TCOD_console_init_root(w: i32, h: i32, title: *const std::os::raw::c_char, fullscreen: bool, renderer: std::os::raw::c_void)

Even though this translation seems straightforward, the research to get to this point was actually quite tedious and led me down quite a few rabbitholes. I've left the details in the appendix in case you're interested.

So this declaration compiles; and a quick test showed that the program still runs without crashing (even though it doesn't do anything yet). Now it's time to make the actual call to TCOD_console_init_root and see if that works.

Except there's a big error here, which I didn't notice when I added that function.

See that last parameter? The TCOD_renderer_t. The Python bindings don't have this parameter in it! I wasn't paying a ton of attention to all the parameters - after all, there's four of them, and I failed to notice that the C version has an extra one...

The documentation doesn't have the types easily accessible, so I just dug around in the source code to figure out the type. Now, if you're using a good IDE, you can just search for the function declaration, and it will give you a way to jump to the definition. In case that isn't working, you can also do a full text search on the source code - although sometimes that can come up with a lot of hits. What I did instead was to do only a full text search in the include directory. Most libraries will have the code separated into two sections - one section being the external details that a client will have to use, and the other being all the implementation details. In C, the public declarations are usually in an include directory. So unless I'm interested in the implementation itself, it's easier to just restrict the search to the include directory. And indeed I found it in console_types.h. It's an enum.

Ok, so a TCOD_renderer_t isn't actually an opaque pointer type. It's an enum. Oops.

For my first pass, I'll just bridge it directly using #[repr(c)] and copy and paste the definition. That actually compiled, although the compiler gives me a bunch of warnings about the camel case style of the enum types. I ignored the warnings. No point in wasting time fixing such a minor detail until I know this approach works.

Then I made the actual call:


TCOD_console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, CString::new("Rogue game").unwrap().into_raw(), false, RendererType::TCOD_RENDERER_SDL);

This seemed overly complex, but all the research I've done so far suggests it's right, so I did a compile and test cycle. It compiled and ran, but it gave me the error: "SDL : cannot load terminal.png"

My current hypothesis is that the function has been called successfully. If I had messed up something in the Rust FFI, or just blatantly called it with the wrong parameters, I would have expected some kind of random crasher, or a segfault, some kind of other mysterious generic error. In this case, it's clearly looking for terminal.png, and it can't find it.

I looked at the original tutorial again, and I'm reminded that I skipped a function call that I made in the original: namely, the call to set the default font to arial10x010.png. This certainly looks like it might be related. Since I didn't set any font, the default is probably "Terminal," and without that font file, the program exits. By quickly looking at where I found the arial png in the first place, I saw various other terminal files that end in png, but not specifically terminal.png.

So now I can can either dig into the documentation to figure out how tcod resolves the font to the filename; or I can just copy one of the terminal pngs over and see what happens; or I can try to also make the FFI call to set the font.

I opted for the last option, because:

This is actually a bigger change than I like to make all at once. Generally, I try to keep the iteration from working program to working program much smaller. For example, even though the original tutorial only made about half a dozen python calls into libtcod, I only ported one of the calls to Rust, and then tested to see if it works. Especially in the early steps of learning something new - in this case, attempting to make an FFI call to a library I haven't used before - I try to test after every chunk of work. And I consider this two chunks - one, setting up the root console, and two - setting up the font. However, it seemed like it might be just as much work to figure out how to get the terminal font working, so I ended up implementing the next chunk of work I was planning to implement.

After I made the associated FFI changes, and added the font call, I got:


TCOD_console_set_custom_font(CString::new("arial10x10.png").unwrap().into_raw(), FontFlags::TCOD_FONT_TYPE_GREYSCALE as i32 | TCOD_FONT_LAYOUT_TCOD as i32, 0, 0);
TCOD_console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, CString::new("Rogue game").unwrap().into_raw(), false, RendererType::TCOD_RENDERER_SDL);

Notice the font type enum has to be cast to an integer. This is because Rust enums aren't just numbers, and arbitrary math can't be done on them. There's also the limitation that a Rust enum has to have mutually exclusive options, and the original C code has "greyscale" and "grayscale" aliased to the same value. I work around that with this stackoverflow answer. Side note: Rust enums don't seem to be a good match for C enums when they are used as bitfields. I should probably look into using something different here - maybe a full Rust struct.

When I ran this code, it works! I saw a window pop up quickly and then terminated immediately, and there are no crashes. It's not much, but it's exactly what I expected these lines of code to do. Successful feature.

Refactoring

However, even though this code works, it still isn't very good, and is nowhere ready for a pull request.

What are the problems with this?

Fundamentally, this all boils down to one problem: I haven't abstracted away the implementation very well. In other words, to program using these bindings, you have to know that you're using libtcod and you have to know it's using FFI, and work around those implementation details.

I can fix this by moving all the bindings into a module. This module will handle all the ugly details of calling out with the FFI, and provide an interface that maps more nicely into Rust. The module will wrap all the call outs to the library with unsafe, and it will contain all the enum and struct definitions that need to be passed back and forth to the C part.

Once I've made these changes, the main code looks like:


tcodrs::console_set_custom_font("arial10x10.png", &[tcodrs::FontFlags::TypeGreyScale, tcodrs::FontFlags::LayoutTcod], 0, 0);
tcodrs::console_init_rooot(SCREEN_WIDTH, SCREEN_HEIGHT, "Rogue game", false, tcodrs::RendererType::Sdl);

This hides away all the implementation details so the client doesn't have to deal with them.

With that out of the way, we can finish up with the rest of the FFI bindings and call them. There are just a few wrinkles:

With all that done, our final main function looks like:


tcodrs::console_set_custom_font("arial10x10.png", &[tcodrs::FontFlags::TypeGreyScale, tcodrs::FontFlags::LayoutTcod], 0, 0);
tcodrs::console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, "Rogue game", false, tcodrs::RendererType::Sdl);

while !tcodrs::console_is_window_closed() {
    tcodrs::console_set_default_foreground(tcodrs::Color::white());
    tcodrs::console_put_char(1, 1, '@', tcodrs::BkgndFlag::None);
    tcodrs::console_flush();
	
    tcodrs::console_wait_for_keypress(false);
}

After testing and running, this seemed to work fine. It also means it's a good place to stop and submit a pull request. I reviewed my own code (the benefit of working on my own side project) and I approved it, and went ahead and committed it to the repo.

Character movement

As I mentioned earlier, it looks like tcod is pretty low level. The tutorial keeps track of its own player position state instead of calling into the library to make updates. Similarly, a quick browse of the libtcod docs doesn't show any player specific functionality (although it does seem to have some image or spriting features).

So, given that, the idea is fairly straightforward: Keep track of our player state, and check to see what the user inputs via the keyboard. Based on that, update our player state, then update the display.

Even though the idea is straightforward, there's still lots of variation in the details. For example, just checking the different tutorials, I see:

I'm sure there's also many other ways to do this.

I ended up putting the state together into one struct, and then pass that to the handlers and have the handlers update the state directly. I find this a good balance for my coding style.

In general, I try to avoid globals, and I find it easy enough to do, so it never really costs me that much more. To be fair, the game state is essentially going to be global to all the key handling actions, since I'll have something that switches on the user input to decide what to do - and that switch is probably going to call a bunch of different possible functions with the game state. Still, at least there's less chance that I accidentally modify it in the sound system or some other crazy area because I happened to use a same name. I've also chosen not to return the action as a return value and have the main loop process that return value, because that creates a dependency between the key handlers and the main loop. Now, there are cases where it's useful, and even required, to separate out the key handling and the actions. But in that case, I want to be very explicit that this is an interface, and so I'd want to add in some more objects to mediate this dependency - for example, some kind of explicity component system. Finally, I'm gathering all the state together in one struct so it's clear that these variables (and only these variables) are related to the game state.

That was a pretty long winded explanation of, frankly, some fairly close design decisions, and I think all the alternatives are reasonable in the right contexts, so this particular implementation shouldn't be treated as the "right" way to do things. I'm choosing it because it's a style I'm comfortable with.

Finally, with all that... the implementation wasn't too complicated. The handle_keys function take the input, update the user state, and return a boolean saying whether the main loop should be terminated:


fn handle_keys(key: tcodrs::Key, player: &mut Player) -> bool {
    match key.vk {
        tcodrs::Keycode::Up => player.y -= 1,
        tcodrs::Keycode::Down => player.y += 1,
        tcodrs::Keycode::Left => player.x -= 1,
        tcodrs::Keycode::right => player.x += 1,
		
        _ => {}
    }

    skey.vk == tcodrs::Keycode::Escape
}

The only caveat is that we need to clear out the current player position on the screen first, before we call handle_keys, because once we update the game state, we will lose the old position. Incidentally, I find this error prone way to do things, so I hope this gets tweaked as we go further along in the tutorial. Usually it's easier to completely clear the display and redraw everything, instead of trying to partially undo some changes and make new ones.

Summary

After each checkpoint it's useful to assess the state of the project and review what happened - a mini post mortem, so to speak.

I can see already that the module file for the bindings is getting unwieldy. I'll have to split that up, and given the rate I'm adding bindings, it will probably be sooner rather than later. On that note, I can also see that I'm typing tcodrs:: a lot in the client code. I haven't decided yet if I'm going to just add a use statement and saving some typing. As the program gets bigger I probably will.

By far the biggest complication was making the FFI bindings. Unfortunately I didn't take accurate time measurements, but I'm very certain that figuring out the right syntax for the bindings took over half the time for this entire part, and maybe even over 80% of the time. Part of this was because of sparse and uneven documentation, and the lack of examples online. And to add insult to injury, the end result isn't particularly complicated or surprising. There were no real gotchas or tricks that needed to be memorized. Fortunately, since the FFI is more or less straightforward, future bindings should be very quick and easy to do. On a larger project I'd probably investigate using bindgen which is an automatic generator, but as a learning experience I don't think this time was wasted and knowing the internals will help a lot when I actually do use bindgen in the future.

All this is really to say that, for all you beginning and intermediate programmers out there, don't get discouraged when seemingly simple statements take a long time to write. It happens to all of us. Similarly, don't assume that someone else was able to write something simple very quickly; there may have been a deceptive amount of work that went into finding all the necessary knowledge.

Rust FFI footnote

This is a gory breakdown of the research I had to do to get the FFI bindings working. It can be skipped without impacting the continuity of this project.

My first attempt was to write the FFI for:


void TCOD_console_init_root(int w, int h, const char * title, bool fullscreen, TCOD_renderer_t renderer)

The first place I looked was the official documentation. (Incidentally, I couldn't find this section in the second edition.)

The code samples use size_t and c_int, but don't explain which types they match to. So from my experience with working with different platforms, I know that different C types can have different sizes depending on the build configuration - for example a C long can be both 32 bit and 64 bit, and so the Rust side and the C side have to be built with the same configuration. I'm guessing c_int corresponds to a C int, but does size_t correspond to the C typedef size_t or something else?

After poking around for a bit, I decided to just use i32, which probabilistically should be right since most desktop machines use a 32 bit int. I'll have to go back and fix this later for some platforms, but making some progress is better than getting blocked completely with no path forward.

My next problem is the title string (const char*). Now the documentation page has examples of passing in raw pointers as a *const u8, so I could do something like that. However, since strings are semantically more than just raw bytes, I looked around some more to see if there was a special bridging type. The first thing that comes up in a web search is std::ffi::CString. That seems like a reasonable choice.

Lastly, I need to bridge the TCOD_renderer_t. That looks like some kind of opaque pointer type. Fortunately the documentation has a section entirely about opaque structs. The bridging type is libc::c_void. (There's a nicer way to do it using a Rust enum in the same section; I'll look at it later. I'll try to get it working first, and then try to improve each of the individual pieces.)

So here is the first version of the FFI bridge:


fn TCOD_console_init_root(w: i32, h: i32, title: ffi::CString, fullscreen: bool, renderer: libc::c_void);

This doesn't compile. There's no reference to libc.

At this point, I've wrestled for quite some time with this single line of code. The last thing I want to do is go learn yet another thing (cargo) and figure out the package management to get libc referenced properly.

Instead, I went online to see if there's some other way to do this. And I find a thread on the Rust web site. It's from a couple of years ago, but it doesn't look like it's been resolved so maybe std::os::raw will still work? Searching for that literal turns up official documentation, which actually looks like the thing I want (a list of C types and their equivalent Rust types).

So the next version of the FFI bridge:


fn TCOD_console_init_root(w: i32, h: i32, title: ffi::CString, fullscreen: bool, renderer: std::os::raw::c_void);

This compiled, but it gave some weird warning about the CString. This time I did a more targeted search for Rust bridging with C strings and I'm able to find the Rust FFI omnibus. These examples are actually calling from C into Rust (while I'm trying to do the reverse, call from Rust into C), but the string principles seem similar enough that between the string parameters and string return values, I'm able to piece together what I need.


fn TCOD_console_init_root(w: i32, h: i32, title: *const std::os::raw::c_Char, fullscreen: bool, renderer: std::os::raw::c_void);

I chose not to convert the primitive int and bool types too to std::os::raw since I'm pretty sure those will work on my particular machine and I had already messed around too much with this. In this case, I'm sure I'll be getting a lot more experience with FFI as I work on this project, so after I get some more experience, I'll go back and reevaluate all the FFI bindings.

At some point throughout this whole mess, I also discover bindgen. I've already done enough of this manually and have no interest in setting up another entire software component and figuring out how to make it parse the C tcod library. I'm sure it will save me time in the future, but for a single project, it's easy to get lost in this rathole of trying to do everything the "right way" and becoming overwhelmed. So for this project, I'll continue to implement it manually. However, this is quite tedious, and for my next project I'll probably look into getting this working.


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.