Setting it up

Installing Rust

To install Rust, go to the Rust homepage and click the Install button.

Creating the roguelike project

Next we need to create the Rust project that will be our roguelike:

$ cargo new --bin roguelike
$ cd roguelike/

This will create two files: Cargo.toml and main.rs in the src directory. Cargo.toml contains metadata for the project and any dependencies you’re going to need (such as libtcod).

main.rs is the file that contains your program. In it must be a function called main which will be executed when you run your program.

Cargo has automatically put a hello world program in your main.rs file. It looks like this:

fn main() {
    println!("Hello, world!");
}

You can run it by typing:

cargo run --release

You will see something like this:

   Compiling roguelike v0.1.0 (file:///home/thomas/tmp/roguelike)
     Running `target/release/roguelike`
Hello, world!

Rust will first compile main.rs and any other files it needs (there are none yet). The resulting binary program will be called roguelike and placed in the target/release directory.

Cargo will then run the program which will print Hello, world!.

Rust (and Cargo) can compile programs in two modes: with optimisations and without. Code compiled without optimisations is really slow, but it’s easier to work with under a debugger. We will be using the optimised mode in the tutorial (with the --release option), to make sure no one is making speed claims without optimisations. If you do need the debug build for something, just drop the --release flag.

Adding libtcod

You must then install the dependencies for tcod — the Rust bindings for libtcod.

Go to the following link and follow the instructions for your platform (Windows, Linux or Mac OS X):

Adding the font file

We will be using the font arial10x10.png that ships with libtcod.

You can download it from here:

Feel free to check out the other fonts in there and pick the one you like.

Be sure to put it in your game directory — next to Cargo.toml!

Showing the @ on screen

To use any external library (i.e. code outside of your project) in Rust, you need to have it in Cargo.toml under [dependencies]:

[dependencies]
tcod = "0.15"

Rust libraries are called crates. Next, add some imports via the use statements:

use tcod::colors::*;
use tcod::console::*;

Like in Python, Rust’s code can be structured under modules. We could always use the full path to a particular function or type (e.g. ::tcod::console::Root), but with the use statement, we can bring it up to the current scope.

So use tcod::console::Root will let us type Root directly. Using the asterisk (*) as the name means to import everything under tcod::console. Which means Root, FontType, BackgroundFlag, etc.

Now we’ll add some constants so we don’t end up littering the source code with a bunch of opaque numbers:

// actual size of the window
const SCREEN_WIDTH: i32 = 80;
const SCREEN_HEIGHT: i32 = 50;

const LIMIT_FPS: i32 = 20; // 20 frames-per-second maximum

Just like in Python, constants are written using uppercase (as a convention, not a hard rule). Unlike Python, the const keyword guarantees that they will not be changed.

As you can see, constants need to have their type specified. In this case it’s i32 or a 32-bit signed integer. Rust is able to infer most types, but they do need to be explicitly mentioned in function definitions and constants.

We will also encapsulate all of our libtcod-related values into a single Rust struct:

struct Tcod {
    root: Root,
}

It only has a single value right now, but we’ll add more later on. Having all the tcod stuff in one place makes it easier to pass it around to functions. And doing it now will save us from gnarly refactors later. The Rust compiler would help us there, but it’s still an annoying chore we can avoid.

And now we can dive into the body of the main function and replace the println! macro with code that sets up the libtcod window and renders a character on it.

To create a window we call Root::initializer(). …​ .init(). In between go custom settings. Default values are going to be used for any option that’s not specified.

let root = Root::initializer()
    .font("arial10x10.png", FontLayout::Tcod)
    .font_type(FontType::Greyscale)
    .size(SCREEN_WIDTH, SCREEN_HEIGHT)
    .title("Rust/libtcod tutorial")
    .init();

let mut tcod = Tcod { root };

A bunch of stuff is happening here so let’s unpack this.

Rust uses let to create new variables, so let root = <value> will create a new variable called root. Variables are immutable by default (so they can’t be changed accidentally), but we will want to change the root console (e.g. by writing characters to it). To do that, we need the mut keyword.

So let mut tcod = <value> means "create a variable called tcod that we can change later". As you can see, we don’t have to specify the type of the variable — Rust will figure it out. If we wanted, we could type it explicitly, though:

let root: Root = Root::initializer(). ... .init();

Now let’s have a look at the window options we’re passing in.

First, we’re setting up a font. Libtcod uses bitmap fonts of various formats. Calling the font methods lets us set our own font using a file name and a font layout. font_type is another option for loading a font.

The font must be in the root of your project, next to Cargo.toml. If you’ve picked a different font than arial10x10.png, make sure to put the right filename in your font method call.

Next we set the window dimensions (width and height in characters) and the text displayed in the window’s title bar.

Calling init will finalise the configuration and actually create the window.

tcod::system::set_fps(LIMIT_FPS);

This line will limit the maximum number of frames per second libtcod will issue. This is useful when you have a realtime game loop. If you block for input (i.e. nothing happens until the player presses a key), it will have no effect.

And speaking of game loops, now’s the time to add one! Let’s render a white @ on the screen until the libtcod window gets closed:

while !tcod.root.window_closed() {
    tcod.root.set_default_foreground(WHITE);
    tcod.root.clear();
    tcod.root.put_char(1, 1, '@', BackgroundFlag::None);
    tcod.root.flush();
    tcod.root.wait_for_keypress(true);
}

Since we’ve set the FPS limit, this loop will be executed 20 times a second, no more.

The window_closed method on the root console returns true if the window was closed and false otherwise. We want to keep going while it’s open so we have to use ! to negate the value.

The next line sets a default foreground colour to white. This is the colour everything will be drawn with unless specified otherwise.

The tcod::colors module contains values for common colours as well as the Color struct that lets you use your own.

Then we clear the console of anything that we drew on the previous frame.

Next we draw the @ character at the coordinates 1, 1 on the screen. The 0, 0 coordinate is at the top left corner of the window.

Using BackgroundFlag::None says to ignore the default background colour.

Calling flush will draw everything on the window at once.

And finally, we also need to call wait_for_keypress even though we’re not processing keyboard input yet. This is because libtcod handles the window manager’s events (including your request to close the window) in the input processing code.

If we didn’t call it, window_close would not work properly and the game would crash or hang.

You can now run it with cargo run --release and bask in your creation. It’s almost a game now!

We will look at input next.

Moving around

So that was cool. Now let’s make our @ move!

We’ll need to keep track of the player’s position. Let’s create variables for x and y and put them right before the game loop:

let mut player_x = SCREEN_WIDTH / 2;
let mut player_y = SCREEN_HEIGHT / 2;

They are mutable (we will change them when the player presses the arrow keys) and initialised to the centre of the screen instead of the top-left corner.

We will split the keyboard handling code into its own function to make our game loop more readable. It will need the root console because that’s where we read the pressed keys from and also the player’s coordinates so we can change them based on the player’s actions.

fn handle_keys(tcod: &mut Tcod, player_x: &mut i32, player_y: &mut i32) -> bool {
    // todo: handle keys

    false
}

A function signature in Rust is fn function_name(parameter: type, …​) → return_type. Here we call our function handle_keys; it accepts three parameters — the root console (of type tcod::console::Root), the x coordinate and the y coordinate (of type i32) — and it returns a boolean value. true says "exit the game", false means "keep going".

The &mut bits before the types are borrowing operators. You can read about them (and the ownership they’re strongly tied to) in the Rust book:

We must pass root as a borrowed value because it would be consumed by the first call to handle_keys otherwise.

If we just passed player_x and player_y by value, handle_keys could only read their values but it could not change them. Since we want to update them based on the key the player pressed, we’ll get them as mutable references. Then we can assign a new value using the dereference operator (e.g. *player_x = 10) and that will show up back in the calling scope.

Right now, the function’s body is empty, except that it always returns false (which means, keep the game going). Let’s add the keyboard stuff.

We use the wait_for_keypress method to get the key and then update the player’s position if it’s one of the arrow keys:

let key = tcod.root.wait_for_keypress(true);
match key {
    // movement keys
    Key { code: Up, .. } => *player_y -= 1,
    Key { code: Down, .. } => *player_y += 1,
    Key { code: Left, .. } => *player_x -= 1,
    Key { code: Right, .. } => *player_x += 1,

    _ => {}
}

Instead of chaining a ton of if/else expressions together, we use the match expression to specify the values we’re interested in and what to do with them.

The key returned by wait_for_keypress is of type tcod::input::Key and has several fields we can look at. Right now all we care about is the code, which tells us the key that was pressed, but there are others for alt, ctrl, etc.

The two dots at the end mean "I don’t care about the other fields". If it wasn’t there, it would not compile until you specified values for every field of the Key struct.

Rust requires that match arms are exhaustive. That means you have to specify all the possible values. However, as we don’t care about the other keys the player could possibly press, we can use a special value that matches everything else. That’s what _ ⇒ {} at the end does.

We could end here, but since we’re doing keyboard stuff anyway, let’s add two more: Alt+Enter to toggle fullscreen mode and Esc to exit the game.

Put these at the beginning of match key:

Key {
    code: Enter,
    alt: true,
    ..
} => {
    // Alt+Enter: toggle fullscreen
    let fullscreen = tcod.root.is_fullscreen();
    tcod.root.set_fullscreen(!fullscreen);
}
Key { code: Escape, .. } => return true, // exit game

And finally we need to use the keyboard input types we have in the code:

use tcod::input::Key;
use tcod::input::KeyCode::*;

Now, we could put it on top of the file next to the existing imports, but Rust lets you place them in individual functions as well, which will make them available only for that function. Since we’ll contain our keyboard-handling code in handle_keys, let’s make it the first thing there.

And finally, we just update the main loop to use our key handling and draw at the player coordinates instead of (1, 1). Put this at the end of the while block:

// handle keys and exit game if needed
let exit = handle_keys(&mut tcod, &mut player_x, &mut player_y);
if exit {
    break;
}

As you can see, we’re passing tcod and the coordinates as mutable references.

Now update our drawing function to use the player coordinates:

tcod.root
    .put_char(player_x, player_y, '@', BackgroundFlag::None);

Continue to the next part.