Porting the Complete Roguelike Tutorial to Rust
gamedev, announcement, tutorial, rust
About a month ago, I’ve finished porting the Python Roguelike Tutorial to Rust using the tcod bindings.
The Python tutorial is a starting point to a lot of beginners. It shows one way of writing a roguelike in Rust and it was useful to validate that the Rust libtcod bindings are complete enough.
You can read the tutorial here:
http://tomassedovic.github.io/roguelike-tutorial/
And here’s the repository if you want to check the code out, report a bug or submit an improvement:
https://github.com/tomassedovic/roguelike-tutorial/
Initially, I tried to follow the original Python tutorial as closely as possible, but Rust is different enough that it just did not make sense. I did strive to keep most of the code organisation and architecture close so that someone familiar with the Python tutorial could just check the differences and get going.
In the end, I’m not sure that was a good idea – I have a feeling that if the game was designed with Rust in mind from the beginning, the code would have been more natural and showcased Rust better.
Some obvious differences from the Python version:
- Rust makes it much harder to use mutable globals, so we’re just passing the extra arguments to the functions. Global variables in Rust are done with
static
, but they requireunsafe
blocks which makes them harder to use and explain. - We’re using the rand crate instead of libtcod’s own random number generator.
- In Python, everything is a (reference-counted) pointer. So you can define your global player object, put it into an array and still access the player directly. In Rust, you make things into pointers explicitly. I could have put the player and NPCs into
Rc
, keep those pointers around and clone them into theobjects
array as well, but that was too verbose. In addition, the Python code uses these to access parent references and Rust can’t do that because of its mutability rules. - Rust functions don’t have default parameters. It’s possible to work around that by using multiple constructors, the builder pattern, wrapping input in a struct +
default::Default
, or usingOption<argument>
but they are all verbose so I just opted in to making everything explicit. - The tutorial/code needs to build on stable Rust, so e.g. for serialisation, I had to use rustc_serialize instead of serde.
Other observations:
- Type inference makes this much less cumbersome than say in Java or C#.
rand::random()
is able to produce any type you need in your context (bool
,f32
, etc.). This is thanks to the compiler knowing which type you want ahead of time. In Python you need different functions for each return value.- indexing an array or a vector requires
usize
but chances are you want to usei32
for your positions, dimensions etc. This can be handled easily by wrapping the game map/level in a struct with the right methods, but to mirror this quick & dirty implementation of the Python guide, the explicit casting gets a bit verbose.
The tutorial is now at a parity with the original, but there’s a few things I’d like to see happen. I’m not sure when I’ll get to them, all these are up for grabs!
- Split the code into multiple files
- Implement monster pathfinding
- general code cleanup:
Once the support for custom derive lands in stable Rust, I’d like to switch to serde for saving/loading the game.