Off-screen consoles

Before we continue, let’s talk about consoles. In libtcod, console is where we draw stuff. We’ve only used one so far — the root console. It is connected to the game window and anything you want to show must eventually make its way there.

We can, however, create so-called offscreen consoles and draw things on them. Doing so will let us add transparency effects or only render a portion of the console. It will also let us stack GUI windows on top of the main game, render the info panel and the map separately, etc.

From now on, we will draw to offscreen consoles and then compose them into the root. It will occupy the whole screen for now.

Put this right after we initialise the root console:

let con = Offscreen::new(SCREEN_WIDTH, SCREEN_HEIGHT);

We will want to pass con to our Tcod struct alongside root:

let mut tcod = Tcod { root, con };

And of course we will need to add the con field to the Tcod struct definition:

struct Tcod {
    root: Root,
    con: Offscreen,
}

Now call the set_default_foreground, clear and put_char methods on con instead of root:

tcod.con.set_default_foreground(colors::WHITE);
tcod.con.clear();
tcod.con.put_char(player_x, player_y, '@', BackgroundFlag::None);

And finally, blit the contents of the new console to the root console to display them. The blit function takes a lot of parameters, but the meaning is straightforward. We take the console we want to blit from (i.e. con), the coordinates where to start and the width and height of the area we want to blit (we’ll blit it all). Then the destination (root), where to start blitting (we’ll use the top-left corner) and finally a foreground and background transparency (0.0 is fully transparent, 1.0 completely opaque).

// blit the contents of "con" to the root console and present it
blit(
    &tcod.con,
    (0, 0),
    (SCREEN_WIDTH, SCREEN_HEIGHT),
    &mut tcod.root,
    (0, 0),
    1.0,
    1.0,
);

Generalising

Now that we have the @ walking around, it would be a good idea to step back and think a bit about the design. Having variables for the player’s coordinates is easy, but it can quickly get out of control when you’re defining things such as HP, bonuses, and inventory. We’re going to take the opportunity to generalize a bit.

Now, there can be such a thing as over-generalization, but we’ll try not to fall in that trap. What we’re going to do is define the player as a game Object. It will hold all position and display information (character and color). The neat thing is that the player will just be one instance of the Object struct — it’s general enough that you can re-use it to define items on the floor, monsters, doors, stairs; anything representable by a character on the screen. Here’s the class, with the initialization, and two common methods move_by and draw. The code for drawing and erasing is the same as the one we used for the player earlier.

/// This is a generic object: the player, a monster, an item, the stairs...
/// It's always represented by a character on screen.
struct Object {
    x: i32,
    y: i32,
    char: char,
    color: Color,
}

impl Object {
    pub fn new(x: i32, y: i32, char: char, color: Color) -> Self {
        Object { x, y, char, color }
    }

    /// move by the given amount
    pub fn move_by(&mut self, dx: i32, dy: i32) {
        self.x += dx;
        self.y += dy;
    }

    /// set the color and then draw the character that represents this object at its position
    pub fn draw(&self, con: &mut dyn Console) {
        con.set_default_foreground(self.color);
        con.put_char(self.x, self.y, self.char, BackgroundFlag::None);
    }
}

The dyn keyword in &mut dyn Console highlights that Console is a trait and not a concrete type (such as a struct or enum). Earlier versions of Rust allowed to say &Type regardless of whether Type was trait or not. And indeed at least in Rust 1.37 2018 this is still allowed, but it will emit a warning. This has real-life implications as Rust’s pointers to traits are twice the size of pointers to structs. This way anyone reading the code (us, our collaborators, people taking over) can always tell whether a pointer signature (reference, box, etc.) is a trait object or a normal pointer.

Now, before the main loop, we will create an actual player Object. We will also add it to an array that will hold all objects in the game. While we’re at it, we’ll add a yellow @ that represents a non-player character to test things out:

// create object representing the player
let player = Object::new(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2, '@', WHITE);

// create an NPC
let npc = Object::new(SCREEN_WIDTH / 2 - 5, SCREEN_HEIGHT / 2, '@', YELLOW);

// the list of objects with those two
let mut objects = [player, npc];

Now we’ll need to do a few changes to make it work. First in handle_keys we’ll use player’s move_by method to change the coordinates. This means we’ll need to pass in (a mutable reference to) the player object.

fn handle_keys(tcod: &mut Tcod, player: &mut Object) -> bool {
    // ...

    // movement keys
    Key { code: Up, .. } => player.move_by(0, -1),
    Key { code: Down, .. } => player.move_by(0, 1),
    Key { code: Left, .. } => player.move_by(-1, 0),
    Key { code: Right, .. } => player.move_by(1, 0),

   // ...
}

and update the way we call the function:

// handle keys and exit game if needed
let player = &mut objects[0];
let exit = handle_keys(&mut tcod, player);

Next, the main loop will now draw all objects like so:

for object in &objects {
    object.draw(&mut tcod.con);
}

And finally, since we’re now handling colour and rendering in the Object::draw method, we can now remove the set_default_foreground and put_char calls from the main loop and instead just clear the offscreen console:

// clear the screen of the previous frame
tcod.con.clear();

And that’s it! We have a fully generic object system. Later we can modify this to have all the info items, monsters and anything else will require.

Here’s the code so far.

The Map

Now let’s build a map which will hold our dungeon! It will be a two-dimensional array of tiles. We’ll define its size on top of the source file to be slightly smaller than the window size. This will leave some space for a panel with stats that we’ll add later.

// size of the map
const MAP_WIDTH: i32 = 80;
const MAP_HEIGHT: i32 = 45;

Next we’ll define colours for the tiles. We’ll have two tiles for now: wall and ground. Let’s define their dark variants. When we add field of view, we’ll have to add a set for when they’re lit.

const COLOR_DARK_WALL: Color = Color { r: 0, g: 0, b: 100 };
const COLOR_DARK_GROUND: Color = Color {
    r: 50,
    g: 50,
    b: 150,
};

Since the map is going to be built from tiles, we need to define them! We’ll start with two values: whether a tile is passable and whether it blocks sight.

It’s good to keep the values separate from the beginning as it will let us have see-through but impassable tiles such as chasms or passable tiles that block sight for secret passages. We’ll create a Tile struct:

/// A tile of the map and its properties
#[derive(Clone, Copy, Debug)]
struct Tile {
    blocked: bool,
    block_sight: bool,
}

impl Tile {
    pub fn empty() -> Self {
        Tile {
            blocked: false,
            block_sight: false,
        }
    }

    pub fn wall() -> Self {
        Tile {
            blocked: true,
            block_sight: true,
        }
    }
}

The #[derive(…​)] bit automatically implements certain behaviours (Rust calls them traits, other languages use interfaces) you list there. Debug is to let us print the Tile’s contents and Clone and Copy will let us copy the values on assignment or function call instead of moving them. So they’ll behave like e.g. integers in this respect.

We don’t want the Copy behaviour for Object (we could accidentally modify a copy instead of the original and get our changes lost for example), but Debug is useful, so let’s add the Debug derive to our Object as well:

#[derive(Debug)]

We’ve also added helper methods to build the two types of Tiles we’re going to be using the most.

And now the map! It’s a two-dimensional array (Vec) of tiles. The full type is Vec<Vec<Tile>> (a vec composed of vecs of tiles). Since we’re going to be passing it around a lot, let’s define a shortcut:

type Map = Vec<Vec<Tile>>;

struct Game {
    map: Map,
}

This let’s use write Map wherever we’d have to write Vec<Vec<Tile>> and it’s also easier to understand.

And we’ve also created a new Game struct. The motivation here is identical to the Tcod struct: there are going to be things we will almost always want to pass together and this will save us some refactoring later.

It will also come in super handy when we get to saving and loading.

Now we’ll build it using nested vec! macros:

fn make_map() -> Map {
    // fill map with "unblocked" tiles
    let mut map = vec![vec![Tile::empty(); MAP_HEIGHT as usize]; MAP_WIDTH as usize];

    map
}

The vec! macro is a shortcut that creates a Vec and fills it with values. For example, vec!['a'; 42] would create a Vec containing the letter 'a' 42 times. We do the same trick above to build a column of tiles and then build the map of those columns.

We can access any tile with map[x][y]. Let’s add two pillars (blocked tiles) to demonstrate that and provide a simple test:

// place two pillars to test the map
map[30][22] = Tile::wall();
map[50][22] = Tile::wall();

(you can also access the tile’s properties directly like so: map[30][22].blocked = true)

Next we need to draw the map on our window. Since we need to draw both the objects and the map, let’s create a new function that renders everything and call it from the main loop.

fn render_all(tcod: &mut Tcod, game: &Game, objects: &[Object]) {
    // draw all objects in the list
    for object in objects {
        object.draw(&mut tcod.con);
    }
}

Still in the same function, we can go through all the tiles and draw them to the screen:

// go through all tiles, and set their background color
for y in 0..MAP_HEIGHT {
    for x in 0..MAP_WIDTH {
        let wall = game.map[x as usize][y as usize].block_sight;
        if wall {
            tcod.con
                .set_char_background(x, y, COLOR_DARK_WALL, BackgroundFlag::Set);
        } else {
            tcod.con
                .set_char_background(x, y, COLOR_DARK_GROUND, BackgroundFlag::Set);
        }
    }
}

And let’s move the blit call to the end of render_all:

// blit the contents of "con" to the root console
blit(
    &tcod.con,
    (0, 0),
    (MAP_WIDTH, MAP_HEIGHT),
    &mut tcod.root,
    (0, 0),
    1.0,
    1.0,
);

We’ve replaced the SCREEN_* dimensions with the MAP ones. From now on, the con offscreen console object will represents the map only.

This gives some space at the bottom for the message log, status bar, etc.

And we need to update its dimensions (in the main fn) as well:

let con = Offscreen::new(MAP_WIDTH, MAP_HEIGHT);

Now that we’ve got the map and rendering updated, let’s actually create it. In main before the game loop:

let game = Game {
    // generate map (at this point it's not drawn to the screen)
    map: make_map(),
};

And don’t forget to call render_all from the main loop too (right before tcod.flush):

// render the screen
render_all(&mut tcod, &game, &objects);

You should be able to see two pillars and walk around the map now!

But wait, there’s something wrong. The pillars show up, but the player can walk over them. That’s easy to fix though, add this check to the beginning of the Object’s move_by method:

/// move by the given amount, if the destination is not blocked
pub fn move_by(&mut self, dx: i32, dy: i32, game: &Game) {  (1)
    if !game.map[(self.x + dx) as usize][(self.y + dy) as usize].blocked {  (2)
        self.x += dx;  (3)
        self.y += dy;
    }
}
1 We need to pass Map in to check if a tile is blocking
2 Only move if the destination is not blocking
3 The movement code is the same

We’ll also need to pass a reference to the map to handle_keys because it calls move_by. This may look annoying now but as the code grows, it will be good to know which functions can see (and change!) what.

fn handle_keys(tcod: &mut Tcod, game: &Game, player: &mut Object) -> bool {  (1)
    //...

    match key {
        // ...

        // movement keys
        Key { code: Up, .. } => player.move_by(0, -1, game),  (2)
        Key { code: Down, .. } => player.move_by(0, 1, game),
        Key { code: Left, .. } => player.move_by(-1, 0, game),
        Key { code: Right, .. } => player.move_by(1, 0, game),

        // ...
    }

    // ...
}
1 Added Game to handle_keys
2 Passing game to move_by

And finally, we need to pass the map to handle_keys from the main loop:

let exit = handle_keys(&mut tcod, &game, player);
There’s a ton of different ways to create the map. One common alternative is one continuous Vec with MAP_HEIGHT * MAP_WIDTH items. To access a tile on (x, y), you would do map[y * MAP_WIDTH + x]. The advantage is that you only do one array lookup instead of two and iterating over every object in the map is faster because they’re all in the same region of memory.
Or you could treat walls and everything else in the map as just another Object and store them there. This would make the game structure simpler (everything is an Object) and more flexible (just add HP to make a wall destructible, or damage to one that’s supposed to be covered with spikes).

Continue to the next part.