|
@@ -12,9 +12,234 @@
|
|
|
|
|
|
In the last chapter, we used binary space partition (BSP) to build a dungeon with rooms. BSP is flexible, and can help you with a lot of problems; in this example, we're going to modify BSP to design an interior dungeon - completely inside a rectangular structure (for example, a castle) and with no wasted space other than interior walls.
|
|
|
|
|
|
+The code for this chapter is converted from *One Knight in the Dungeon*'s prison levels.
|
|
|
+
|
|
|
## Scaffolding
|
|
|
|
|
|
+We'll start by making a new file, `map_builders/bsp_interior.rs` and putting in the same initial boilerplate that we used in the previous chapter:
|
|
|
+
|
|
|
+```rust
|
|
|
+use super::{MapBuilder, Map, Rect, apply_room_to_map,
|
|
|
+ TileType, Position, spawner, SHOW_MAPGEN_VISUALIZER};
|
|
|
+use rltk::RandomNumberGenerator;
|
|
|
+use specs::prelude::*;
|
|
|
+
|
|
|
+pub struct BspInteriorBuilder {
|
|
|
+ map : Map,
|
|
|
+ starting_position : Position,
|
|
|
+ depth: i32,
|
|
|
+ rooms: Vec<Rect>,
|
|
|
+ history: Vec<Map>,
|
|
|
+ rects: Vec<Rect>
|
|
|
+}
|
|
|
+
|
|
|
+impl MapBuilder for BspInteriorBuilder {
|
|
|
+ fn get_map(&self) -> Map {
|
|
|
+ self.map.clone()
|
|
|
+ }
|
|
|
+
|
|
|
+ fn get_starting_position(&self) -> Position {
|
|
|
+ self.starting_position.clone()
|
|
|
+ }
|
|
|
+
|
|
|
+ fn get_snapshot_history(&self) -> Vec<Map> {
|
|
|
+ self.history.clone()
|
|
|
+ }
|
|
|
+
|
|
|
+ fn build_map(&mut self) {
|
|
|
+ // We should do something here
|
|
|
+ }
|
|
|
+
|
|
|
+ fn spawn_entities(&mut self, ecs : &mut World) {
|
|
|
+ for room in self.rooms.iter().skip(1) {
|
|
|
+ spawner::spawn_room(ecs, room, self.depth);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fn take_snapshot(&mut self) {
|
|
|
+ if SHOW_MAPGEN_VISUALIZER {
|
|
|
+ let mut snapshot = self.map.clone();
|
|
|
+ for v in snapshot.revealed_tiles.iter_mut() {
|
|
|
+ *v = true;
|
|
|
+ }
|
|
|
+ self.history.push(snapshot);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl BspInteriorBuilder {
|
|
|
+ pub fn new(new_depth : i32) -> BspInteriorBuilder {
|
|
|
+ BspInteriorBuilder{
|
|
|
+ map : Map::new(new_depth),
|
|
|
+ starting_position : Position{ x: 0, y : 0 },
|
|
|
+ depth : new_depth,
|
|
|
+ rooms: Vec::new(),
|
|
|
+ history: Vec::new(),
|
|
|
+ rects: Vec::new()
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+We'll also change our random builder function in `map_builders/mod.rs` to once again lie to the user and always "randomly" pick the new algorithm:
|
|
|
+
|
|
|
+```rust
|
|
|
+pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> {
|
|
|
+ /*let mut rng = rltk::RandomNumberGenerator::new();
|
|
|
+ let builder = rng.roll_dice(1, 2);
|
|
|
+ match builder {
|
|
|
+ 1 => Box::new(BspDungeonBuilder::new(new_depth)),
|
|
|
+ _ => Box::new(SimpleMapBuilder::new(new_depth))
|
|
|
+ }*/
|
|
|
+ Box::new(BspInteriorBuilder::new(new_depth))
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Subdividing into rooms
|
|
|
+
|
|
|
+We're not going to achieve a *perfect* subdivision due to rounding issues, but we can get pretty close. Certainly good enough for a game! We put together a `build` function that is quite similar to the one from the previous chapter:
|
|
|
+
|
|
|
+```rust
|
|
|
+fn build(&mut self) {
|
|
|
+ let mut rng = RandomNumberGenerator::new();
|
|
|
+
|
|
|
+ self.rects.clear();
|
|
|
+ self.rects.push( Rect::new(1, 1, self.map.width-2, self.map.height-2) ); // Start with a single map-sized rectangle
|
|
|
+ let first_room = self.rects[0];
|
|
|
+ self.add_subrects(first_room, &mut rng); // Divide the first room
|
|
|
+
|
|
|
+ let rooms = self.rects.clone();
|
|
|
+ for r in rooms.iter() {
|
|
|
+ let room = *r;
|
|
|
+ //room.x2 -= 1;
|
|
|
+ //room.y2 -= 1;
|
|
|
+ self.rooms.push(room);
|
|
|
+ for y in room.y1 .. room.y2 {
|
|
|
+ for x in room.x1 .. room.x2 {
|
|
|
+ let idx = self.map.xy_idx(x, y);
|
|
|
+ if idx > 0 && idx < ((self.map.width * self.map.height)-1) as usize {
|
|
|
+ self.map.tiles[idx] = TileType::Floor;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ self.take_snapshot();
|
|
|
+ }
|
|
|
+
|
|
|
+ let start = self.rooms[0].center();
|
|
|
+ self.starting_position = Position{ x: start.0, y: start.1 };
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Lets look at what this does:
|
|
|
+
|
|
|
+1. We create a new random number generator.
|
|
|
+2. We clear the `rects` list, and add a rectangle covering the whole map we intend to use.
|
|
|
+3. We call a magical function `add_subrects` on this rectangle. More on that in a minute.
|
|
|
+4. We copy the rooms list, to avoid borring issues.
|
|
|
+5. For each room, we add it to the rooms list - and carve it out of the map. We also take a snapshot.
|
|
|
+6. We start the player in the first room.
|
|
|
+
|
|
|
+The `add_subrects` function in this case does all the hard work:
|
|
|
+
|
|
|
+```rust
|
|
|
+fn add_subrects(&mut self, rect : Rect, rng : &mut RandomNumberGenerator) {
|
|
|
+ // Remove the last rect from the list
|
|
|
+ if !self.rects.is_empty() {
|
|
|
+ self.rects.remove(self.rects.len() - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate boundaries
|
|
|
+ let width = rect.x2 - rect.x1;
|
|
|
+ let height = rect.y2 - rect.y1;
|
|
|
+ let half_width = width / 2;
|
|
|
+ let half_height = height / 2;
|
|
|
+
|
|
|
+ let split = rng.roll_dice(1, 4);
|
|
|
+
|
|
|
+ if split <= 2 {
|
|
|
+ // Horizontal split
|
|
|
+ let h1 = Rect::new( rect.x1, rect.y1, half_width-1, height );
|
|
|
+ self.rects.push( h1 );
|
|
|
+ if half_width > MIN_ROOM_SIZE { self.add_subrects(h1, rng); }
|
|
|
+ let h2 = Rect::new( rect.x1 + half_width, rect.y1, half_width, height );
|
|
|
+ self.rects.push( h2 );
|
|
|
+ if half_width > MIN_ROOM_SIZE { self.add_subrects(h2, rng); }
|
|
|
+ } else {
|
|
|
+ // Vertical split
|
|
|
+ let v1 = Rect::new( rect.x1, rect.y1, width, half_height-1 );
|
|
|
+ self.rects.push(v1);
|
|
|
+ if half_height > MIN_ROOM_SIZE { self.add_subrects(v1, rng); }
|
|
|
+ let v2 = Rect::new( rect.x1, rect.y1 + half_height, width, half_height );
|
|
|
+ self.rects.push(v2);
|
|
|
+ if half_height > MIN_ROOM_SIZE { self.add_subrects(v2, rng); }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Lets take a look at what this function does:
|
|
|
+
|
|
|
+1. If the `rects` list isn't empty, we remove the last item from the list. This has the effect of removing the last rectangle we added - so when we start, we are removing the rectangle covering the *whole* map. Later on, we are removing a rectangle because we are dividing it. This way, we won't have overlaps.
|
|
|
+2. We calculate the width and height of the rectangle, and well as half of the width and height.
|
|
|
+3. We roll a dice. There's a 50% chance of a horizontal or vertical split.
|
|
|
+4. If we're splitting horizontally:
|
|
|
+ 1. We make `h1` - a new rectangle. It covers the left half of the parent rectangle.
|
|
|
+ 2. We add `h1` to the `rects` list.
|
|
|
+ 3. If `half_width` is bigger than `MIN_ROOM_SIZE`, we recursively call `add_subrects` again, with `h1` as the target rectangle.
|
|
|
+ 4. We make `h2` - a new rectangle covering the right side of the parent rectangle.
|
|
|
+ 5. We add `h2` to the `rects` list.
|
|
|
+ 6. If `half_width` is bigger than `MIN_ROOM_SIZE`, we recursively call `add_subrects` again, with `h2` as the target rectangle.
|
|
|
+5. If we're splitting vertically, it's the same as (4) - but with top and bottom rectangles.
|
|
|
+
|
|
|
+Conceptually, this starts with a rectangle:
|
|
|
+```
|
|
|
+#################################
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+# #
|
|
|
+#################################
|
|
|
+```
|
|
|
+
|
|
|
+A horizontal split would yield the following:
|
|
|
+
|
|
|
+```
|
|
|
+#################################
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+#################################
|
|
|
+```
|
|
|
+
|
|
|
+The next split might be vertical:
|
|
|
+
|
|
|
+```
|
|
|
+#################################
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+################ #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+# # #
|
|
|
+#################################
|
|
|
+```
|
|
|
+
|
|
|
+This repeats until we have a lot of small rooms.
|
|
|
|
|
|
+You can `cargo run` the code right now, to see the rooms appearing.
|
|
|
|
|
|
**The source code for this chapter may be found [here](https://github.com/thebracket/rustrogueliketutorial/tree/master/chapter-26-bsp-interiors)**
|
|
|
|