About this tutorial
This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!
If you enjoy this and would like me to keep writing, please consider supporting my Patreon.
Yadda
Now that we have a framework to allow us to make a new map, lets do so! Nethack started out using a relatively simple map generation system that still produces satisfying maps. You subdivide your map rectangle into ever-smaller rectangles, sub-dividing each rectangle in turn - until you hit a minimum size. Then you randomly join them together to give a more interesting map.
We'll start by making a new file in map_builders
- bsp_dungeon.rs
. A skeleton implementation of a new builder goes here:
use super::{MapBuilder, Map, Rect, apply_room_to_map,
apply_horizontal_tunnel, apply_vertical_tunnel, TileType,
Position, spawner};
use rltk::RandomNumberGenerator;
use specs::prelude::*;
pub struct BspDungeonBuilder {}
impl MapBuilder for BspDungeonBuilder {
fn build(new_depth: i32) -> (Map, Position) {
let mut map = Map::new(new_depth);
(map, Position{x:0, y:0})
}
fn spawn(map : &Map, ecs : &mut World, new_depth: i32) {
for room in map.rooms.iter().skip(1) {
spawner::spawn_room(ecs, room, new_depth);
}
}
}
This makes an unusable map - but gives us a starting point. We'll modify mod.rs
to always use this builder, while we work on it:
pub fn build_random_map(new_depth: i32) -> (Map, Position) {
BspDungeonBuilder::build(new_depth)
}
pub fn spawn(map : &Map, ecs : &mut World, new_depth: i32) {
BspDungeonBuilder::spawn(map, ecs, new_depth);
}
We'll worry about swapping out map types later. Onto making the map! Note that this implementation is ported from my C++ game, One Knight in the Dungeon. We'll start with room generation:
fn build(new_depth: i32) -> (Map, Position) {
let mut map = Map::new(new_depth);
let mut rng = RandomNumberGenerator::new();
let mut rects : Vec<Rect> = Vec::new(); // Vector to hold our rectangles as we divide
rects.push( Rect::new(2, 2, map.width-5, map.height-5) ); // Start with a single map-sized rectangle
let first_room = rects[0];
add_subrects(&mut rects, first_room); // Divide the first room
// Up to 240 times, we get a random rectangle and divide it. If its possible to squeeze a
// room in there, we place it and add it to the rooms list.
let mut n_rooms = 0;
while n_rooms < 240 {
let rect = get_random_rect(&mut rects, &mut rng);
let candidate = get_random_sub_rect(rect, &mut rng);
if is_possible(&mut map, candidate) {
apply_room_to_map(&mut map, &candidate);
map.rooms.push(candidate);
add_subrects(&mut rects, rect);
}
n_rooms += 1;
}
let player_start = map.rooms[0].center();
(map, Position{ x : player_start.0, y : player_start.1 })
}
So what on Earth does this do?
Rect
structures.add_subrects
, passing it the rectangle list - and the first room. We'll implement that in a minute, but what it does is: it divides the rectangle into four quadrants, and adds each of the quadrants to the rectangle list.get_random_rect
to retrieve a random rectangle from the rectangles list.get_random_sub_rect
using this rectangle as an outer boundary. It creates a random room from 3 to 10 tiles in size (on each axis), somewhere within the parent rectangle.is_possible
if the candidate can be drawn to the map; every tile must be within the map boundaries, and not already a room. If it IS possible:
add_subrects
to sub-divide the rectangle we just used (not the candidate!).If you cargo run
now, you will be in a room with no exits. That's a great start.
Now, we sort the rooms by left coordinate. You don't have to do this, but it helps make connected rooms line up.
map.rooms.sort_by(|a,b| a.x1.cmp(&b.x1) );
sort_by
takes a closure - that is, an inline function (known as a "lambda" in other languages) as a parameter. You could specify a whole other function if you wanted to, or implement traits on Rect
to make it sortable - but this is easy enough. It sorts by comparing the x1
value of each rectangle.
Now we'll add some corridors:
for i in 0..map.rooms.len()-1 {
let room = map.rooms[i];
let next_room = map.rooms[i+1];
let start_x = room.x1 + (rng.roll_dice(1, i32::abs(room.x1 - room.x2))-1);
let start_y = room.y1 + (rng.roll_dice(1, i32::abs(room.y1 - room.y2))-1);
let end_x = next_room.x1 + (rng.roll_dice(1, i32::abs(next_room.x1 - next_room.x2))-1);
let end_y = next_room.y1 + (rng.roll_dice(1, i32::abs(next_room.y1 - next_room.y2))-1);
draw_corridor(&mut map, start_x, start_y, end_x, end_y);
}
This iterates the rooms list, ignoring the last one. It fetches the current room, and the next one in the list and calculates a random location (start_x
/start_y
and end_x
/end_y
) within each room. It then calls the mysterious draw_corridor
function with these coordinates. Draw corridor adds a line from the start to the end, using only north/south or east/west (it can give 90-degree bends). It won't give you a staggered, hard to navigate perfect line like Bresenham would.
Finally, we need to wrap up and create the exit:
let stairs = map.rooms[map.rooms.len()-1].center();
let stairs_idx = map.xy_idx(stairs.0, stairs.1);
map.tiles[stairs_idx] = TileType::DownStairs;
We place the exit in the last room, guaranteeing that the poor player has a ways to walk.
If you cargo run
now, you'll see something like this:
We have a different dungeon, this one quite suited to sewers or similar.
Rather than always using the BSP sewer algorithm, we would like to sometimes use one or the other. In map_builders/mod.rs
, replace the build
function:
pub fn build_random_map(new_depth: i32) -> (Map, Position) {
let mut rng = rltk::RandomNumberGenerator::new();
let builder = rng.roll_dice(1, 2);
match builder {
1 => SimpleMapBuilder::build(new_depth),
_ => BspDungeonBuilder::build(new_depth)
}
}
Now when you play, it's a coin toss what type of map you encounter. The spawn
functions for the types are the same - so we're not going to worry about map builder state until the next chapter.
You've refactored your map building into a new module, and built a simple BSP (Binary Space Partitioning) based map. The game randomly picks a map type, and you have more variety. The next chapter will further refactor map generation, and introduce another technique.
The source code for this chapter may be found here
Copyright (C) 2019, Herbert Wolverson.