Last time we have left of our dungeon generation with some hardcoded room positions. This time we will improve on that by adding some proper randomization.
The complete code for this part can be found here: https://github.com/maciekglowka/hike_deck/tree/part_10
We are going to take a bit of a similar approach, as with the tunnel creation. We will make a common trait for the room generation process, so we can easily swap different algorithms. (like different tunneler types previously). That should allow us in the future to be a bit more flexible with our board designs and include special areas like temples or arenas next to the typical chambers.
So, the trait:
// in board/dungeon/room.rs
pub trait RoomGenerator {
fn generate(&self) -> GeneratorResult;
}
pub struct GeneratorResult {
pub rooms: Vec<Room>,
// connections are defined by room indices
pub connections: Vec<(usize, usize)>
}
The trait defines only a single method - one creating the room layout. As you can see in the result, apart from the rooms, we also include the desired connections between them. This way we will ensure that our chambers are logically joined together - depending on the algorithm's rules.
Now let's move to the first (and for now only) implementation. We are going to take a very simple bubbling
approach here:
(0, 0)
and push it into a listIn order to control our randomization a little bit, we will provide the room generator with some initial parameters - like the desired room count, size ranges etc.
Here is the initial code (I hope the comments make it clear enough):
pub struct BubbleGenerator {
// bounds for a random room count
pub room_count: (u32, u32),
// min max room size
pub room_size: (u32, u32),
// min distance between rooms
pub room_padding: Option<u32>,
pub extra_connection_chance: f64
}
impl BubbleGenerator {
fn random_dim(&self) -> (i32, i32) {
let mut rng = thread_rng();
(
rng.gen_range(self.room_size.0..=self.room_size.1) as i32,
rng.gen_range(self.room_size.0..=self.room_size.1) as i32
)
}
}
impl RoomGenerator for BubbleGenerator {
fn generate(&self) -> GeneratorResult {
let mut rng = thread_rng();
let mut connections = Vec::new();
let (w, h) = self.random_dim();
let mut rooms = vec![
Room::new(
Vector2Int::new(0, 0),
Vector2Int::new(w, h)
)
];
// helper value for random point bounds
let max_dist = self.room_size.1 as i32;
let count = rng.gen_range(self.room_count.0..=self.room_count.1);
for _ in 0..=count {
loop {
// take a random existing room as a base
let prev_idx = rng.gen_range(0..rooms.len());
// pick a random point around prev's centre
let centre = rooms[prev_idx].centre();
let a = Vector2Int::new(
rng.gen_range(centre.x - max_dist..=centre.x + max_dist),
rng.gen_range(centre.y - max_dist..=centre.y + max_dist)
);
// get random room size
let (w, h) = self.random_dim();
// get a second corner in a random direction
let b = Vector2Int::new(
a.x + *[-w, w].choose(&mut rng).unwrap(),
a.y + *[-h, h].choose(&mut rng).unwrap()
);
let r = Room::new(a, b);
// check for overlaps with the other rooms
if rooms.iter().any(|other| r.intersects(other, self.room_padding)) {
continue;
}
connections.push((prev_idx, rooms.len()));
// try creating a second connection
if rng.gen_bool(self.extra_connection_chance) {
connections.push((rng.gen_range(0..rooms.len()), rooms.len()));
}
rooms.push(Room::new(a, b));
// if the room is valid, we can break the randomize loop
// and move to the next one
break;
}
}
GeneratorResult { rooms, connections }
}
}
In order for the above to work, we also have to add two more helper methods to the Room
struct:
impl Room {
// cut
pub fn centre(&self) -> Vector2Int {
Vector2Int::new((self.b.x+self.a.x) / 2, (self.b.y+self.a.y) / 2)
}
pub fn intersects(&self, other: &Room, border: Option<u32>) -> bool {
let b = match border {
Some(a) => a as i32,
None => 0
};
!(
other.a.x > self.b.x + b ||
other.b.x < self.a.x - b ||
other.a.y > self.b.y + b ||
other.b.y < self.a.y - b
)
}
}
The border
parameter in the intersects
method allows us to add some extra spacing around the rooms - if a positive value is provided. But you may also omit it for more organic layouts.
Now let's change the Area
struct so it accepts the RoomGenerator
object (as we did with the tunneler):
pub struct Area {
pub rooms: Vec<Room>,
pub paths: Vec<Vec<Vector2Int>>,
pub tunneler: Box<dyn Tunneler>,
pub room_generator: Box<dyn RoomGenerator>
}
impl Area {
pub fn new(tunneler: Box<dyn Tunneler>, room_generator: Box<dyn RoomGenerator>) -> Self {
Area { rooms: Vec::new(), paths: Vec::new(), tunneler, room_generator }
}
pub fn generate_rooms(&mut self) {
let result = self.room_generator.generate();
self.rooms = result.rooms;
self.paths = result.connections.iter()
.map(|a| self.join_rooms(&self.rooms[a.0], &self.rooms[a.1]))
.collect();
}
// cut
}
One last thing left to do - include the generator object when spawning the board. You can fiddle with it's parameters and see how it affects the dungeon layout. You can even go further and use different parameters for each of the areas.
pub fn spawn_map(
mut commands: Commands,
mut current: ResMut<CurrentBoard>
) {
let mut dungeon = Dungeon::new(2);
for idx in 0..4 {
let tun = match idx % 2 {
0 => Box::new(tunneler::LShapeTunneler) as Box<dyn tunneler::Tunneler>,
_ => Box::new(tunneler::RandomTunneler) as Box<dyn tunneler::Tunneler>
};
let gen = Box::new(room::BubbleGenerator {
room_count: (3, 5),
room_size: (4, 8),
room_padding: Some(2),
extra_connection_chance: 0.25
}) as Box<dyn room::RoomGenerator>;
dungeon.add_area(Area::new(tun, gen))
}
dungeon.generate();
current.tiles = HashMap::new();
for v in dungeon.to_tiles() {
let tile = commands.spawn((
Position { v },
Tile
))
.id();
current.tiles.insert(v, tile);
}
}
Anyways, after running the game now you should already be able to see something that might resemble a proper game map. Probably our characters will be outside of the board now, but we will handle that in the next part (or the one after that ;)