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.
Here’s the complete code so far.
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);
Here’s the complete code so far.
Continue to the next part.