Implemented BSP
This commit is contained in:
parent
36a72c5789
commit
394e8203ec
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "mapgen"
|
name = "mapgen"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
authors = ["Krzysztof Langner <klangner@gmail.com>"]
|
authors = ["Krzysztof Langner <klangner@gmail.com>"]
|
||||||
description = "Map generator for games (dungeons, worlds etc.)"
|
description = "Map generator for games (dungeons, worlds etc.)"
|
||||||
keywords = ["game", "map", "map-generator"]
|
keywords = ["game", "map", "map-generator"]
|
||||||
|
|
|
@ -13,7 +13,7 @@ Generate procedural maps for games. [Try it in the browser](https://klangner.git
|
||||||
|
|
||||||
* Map generators
|
* Map generators
|
||||||
* [ ] BSP Interior
|
* [ ] BSP Interior
|
||||||
* [ ] BSP Room
|
* [x] BSP Rooms
|
||||||
* [x] Cellular automata
|
* [x] Cellular automata
|
||||||
* [ ] Diffusion-Limited Aggregation (DLA)
|
* [ ] Diffusion-Limited Aggregation (DLA)
|
||||||
* [ ] Drunkard's walk
|
* [ ] Drunkard's walk
|
||||||
|
@ -35,7 +35,7 @@ Generate procedural maps for games. [Try it in the browser](https://klangner.git
|
||||||
|
|
||||||
Add dependency to your project
|
Add dependency to your project
|
||||||
```
|
```
|
||||||
mapgen = "0.1"
|
mapgen = "0.2"
|
||||||
```
|
```
|
||||||
|
|
||||||
Using single map generator:
|
Using single map generator:
|
||||||
|
|
|
@ -6,6 +6,7 @@ use mapgen::dungeon::{
|
||||||
map::TileType,
|
map::TileType,
|
||||||
cellular_automata::CellularAutomataGen,
|
cellular_automata::CellularAutomataGen,
|
||||||
simple_rooms::SimpleRoomsGen,
|
simple_rooms::SimpleRoomsGen,
|
||||||
|
bsp_rooms::BspRoomsGen,
|
||||||
starting_point::{AreaStartingPosition, XStart, YStart},
|
starting_point::{AreaStartingPosition, XStart, YStart},
|
||||||
cull_unreachable::CullUnreachable,
|
cull_unreachable::CullUnreachable,
|
||||||
distant_exit::DistantExit,
|
distant_exit::DistantExit,
|
||||||
|
@ -63,12 +64,30 @@ impl World {
|
||||||
tiles }
|
tiles }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_bsp_rooms(width: u32, height: u32, seed: u32) -> World {
|
||||||
|
World::print_map_info(format!("BSP Rooms with the seed: {}", seed));
|
||||||
|
let mut rng = StdRng::seed_from_u64(seed as u64);
|
||||||
|
let map = MapBuilder::new(BspRoomsGen::new())
|
||||||
|
.with(NearestCorridors::new())
|
||||||
|
.build_map_with_rng(width as usize, height as usize, &mut rng);
|
||||||
|
let tiles = (0..map.tiles.len())
|
||||||
|
.map(|i| if map.tiles[i] == TileType::Floor {Cell::Floor} else {Cell::Wall})
|
||||||
|
.collect();
|
||||||
|
World {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
tiles }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_random(width: u32, height: u32, seed: u32) -> World {
|
pub fn new_random(width: u32, height: u32, seed: u32) -> World {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
if rng.gen::<f32>() < 0.5 {
|
let px = rng.gen::<f32>();
|
||||||
|
if px < 0.3333 {
|
||||||
World::new_cellular_automata(width, height, seed)
|
World::new_cellular_automata(width, height, seed)
|
||||||
} else {
|
} else if px < 0.6666 {
|
||||||
World::new_simple_rooms(width, height, seed)
|
World::new_simple_rooms(width, height, seed)
|
||||||
|
} else {
|
||||||
|
World::new_bsp_rooms(width, height, seed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
|
||||||
<a class="dropdown-item" id="cellular-automata-option">Cellular Automata</a>
|
<a class="dropdown-item" id="cellular-automata-option">Cellular Automata</a>
|
||||||
<a class="dropdown-item" id="simple-rooms-option">Simple Rooms</a>
|
<a class="dropdown-item" id="simple-rooms-option">Simple Rooms</a>
|
||||||
|
<a class="dropdown-item" id="bsp-rooms-option">BSP Rooms</a>
|
||||||
<a class="dropdown-item" id="random-option">Random Generator</a>
|
<a class="dropdown-item" id="random-option">Random Generator</a>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -32,6 +32,12 @@ function newSimpleRooms() {
|
||||||
requestAnimationFrame(renderLoop);
|
requestAnimationFrame(renderLoop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function newBspRooms() {
|
||||||
|
var seed = Date.now();
|
||||||
|
world = World.new_bsp_rooms(width, height, seed);
|
||||||
|
requestAnimationFrame(renderLoop);
|
||||||
|
}
|
||||||
|
|
||||||
function newRandomGen() {
|
function newRandomGen() {
|
||||||
var seed = Date.now();
|
var seed = Date.now();
|
||||||
world = World.new_random(width, height, seed);
|
world = World.new_random(width, height, seed);
|
||||||
|
@ -101,4 +107,5 @@ newRandomGen();
|
||||||
// Connect UI element
|
// Connect UI element
|
||||||
document.getElementById('cellular-automata-option').addEventListener('click', newCellularAutomata);
|
document.getElementById('cellular-automata-option').addEventListener('click', newCellularAutomata);
|
||||||
document.getElementById('simple-rooms-option').addEventListener('click', newSimpleRooms);
|
document.getElementById('simple-rooms-option').addEventListener('click', newSimpleRooms);
|
||||||
|
document.getElementById('bsp-rooms-option').addEventListener('click', newBspRooms);
|
||||||
document.getElementById('random-option').addEventListener('click', newRandomGen);
|
document.getElementById('random-option').addEventListener('click', newRandomGen);
|
||||||
|
|
|
@ -6,8 +6,12 @@ use rand::prelude::*;
|
||||||
/// Generate random number between start (inclusive) and end (exclusive).
|
/// Generate random number between start (inclusive) and end (exclusive).
|
||||||
pub fn random_range(rng: &mut StdRng, start: usize, end: usize) -> usize {
|
pub fn random_range(rng: &mut StdRng, start: usize, end: usize) -> usize {
|
||||||
let max = (end - start) as u32;
|
let max = (end - start) as u32;
|
||||||
|
if max == 0 {
|
||||||
|
start
|
||||||
|
} else {
|
||||||
((rng.next_u32() % max) + start as u32) as usize
|
((rng.next_u32() % max) + start as u32) as usize
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// ------------------------------------------------------------------------------------------------
|
/// ------------------------------------------------------------------------------------------------
|
||||||
/// Module unit tests
|
/// Module unit tests
|
||||||
|
|
138
src/dungeon/bsp_rooms.rs
Normal file
138
src/dungeon/bsp_rooms.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
//! Random rooms map generator.
|
||||||
|
//!
|
||||||
|
//! Try to generate rooms of different size to fill the map area.
|
||||||
|
//! Rooms will not overlap.
|
||||||
|
//!
|
||||||
|
//! Example generator usage:
|
||||||
|
//! ```
|
||||||
|
//! use rand::prelude::*;
|
||||||
|
//! use mapgen::dungeon::{
|
||||||
|
//! MapGenerator,
|
||||||
|
//! bsp_rooms::BspRoomsGen
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! let mut rng = StdRng::seed_from_u64(100);
|
||||||
|
//! let gen = BspRoomsGen::new();
|
||||||
|
//! let map = gen.generate_map(80, 50, &mut rng);
|
||||||
|
//!
|
||||||
|
//! assert_eq!(map.width, 80);
|
||||||
|
//! assert_eq!(map.height, 50);
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
|
||||||
|
use rand::prelude::*;
|
||||||
|
use super::MapGenerator;
|
||||||
|
use crate::common::geometry::Rect;
|
||||||
|
use crate::common::random;
|
||||||
|
use super::map::{Map, TileType};
|
||||||
|
|
||||||
|
|
||||||
|
pub struct BspRoomsGen {
|
||||||
|
max_split: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapGenerator for BspRoomsGen {
|
||||||
|
fn generate_map(&self, width: usize, height: usize, rng : &mut StdRng) -> Map {
|
||||||
|
self.build_rooms(width, height, rng)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BspRoomsGen {
|
||||||
|
pub fn new() -> Box<BspRoomsGen> {
|
||||||
|
Box::new(BspRoomsGen {
|
||||||
|
max_split: 240,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_rooms(&self, width: usize, height: usize, rng : &mut StdRng) -> Map {
|
||||||
|
let mut map = Map::new(width, height);
|
||||||
|
let mut rects: Vec<Rect> = Vec::new();
|
||||||
|
// Start with a single map-sized rectangle
|
||||||
|
rects.push( Rect::new(2, 2, (width-5) as i32, (height-5) as i32) );
|
||||||
|
let first_room = rects[0];
|
||||||
|
rects.append(&mut self.split_into_subrects(first_room)); // Divide the first room
|
||||||
|
|
||||||
|
// Up to max_split 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 < self.max_split {
|
||||||
|
let rect = self.get_random_rect(rng, &rects);
|
||||||
|
let candidate = self.get_random_sub_rect(rect, rng);
|
||||||
|
|
||||||
|
if self.is_possible(candidate, &map) {
|
||||||
|
map.add_room(candidate);
|
||||||
|
rects.append(&mut self.split_into_subrects(rect));
|
||||||
|
}
|
||||||
|
n_rooms += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
map
|
||||||
|
}
|
||||||
|
|
||||||
|
fn split_into_subrects(&self, rect: Rect) -> Vec<Rect> {
|
||||||
|
let mut rects: Vec<Rect> = Vec::new();
|
||||||
|
let width = i32::abs(rect.x1 - rect.x2);
|
||||||
|
let height = i32::abs(rect.y1 - rect.y2);
|
||||||
|
let half_width = i32::max(width / 2, 1);
|
||||||
|
let half_height = i32::max(height / 2, 1);
|
||||||
|
|
||||||
|
rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height ));
|
||||||
|
rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width, half_height ));
|
||||||
|
rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width, half_height ));
|
||||||
|
rects.push(Rect::new( rect.x1 + half_width, rect.y1 + half_height, half_width, half_height ));
|
||||||
|
|
||||||
|
rects
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_random_rect(&self, rng : &mut StdRng, rects: &Vec<Rect>) -> Rect {
|
||||||
|
if rects.len() == 1 { return rects[0]; }
|
||||||
|
let idx = random::random_range(rng, 0, rects.len());
|
||||||
|
rects[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_random_sub_rect(&self, rect: Rect, rng: &mut StdRng) -> Rect {
|
||||||
|
let mut result = rect;
|
||||||
|
let rect_width = i32::abs(rect.x1 - rect.x2);
|
||||||
|
let rect_height = i32::abs(rect.y1 - rect.y2);
|
||||||
|
|
||||||
|
let w = usize::max(3, random::random_range(rng, 1, usize::min(rect_width as usize, 20))) + 1;
|
||||||
|
let h = usize::max(3, random::random_range(rng, 1, usize::min(rect_height as usize, 20))) + 1;
|
||||||
|
|
||||||
|
result.x1 += random::random_range(rng, 0, 6) as i32;
|
||||||
|
result.y1 += random::random_range(rng, 0, 6) as i32;
|
||||||
|
result.x2 = result.x1 + w as i32;
|
||||||
|
result.y2 = result.y1 + h as i32;
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_possible(&self, rect: Rect, map: &Map) -> bool {
|
||||||
|
let mut expanded = rect;
|
||||||
|
expanded.x1 -= 2;
|
||||||
|
expanded.x2 += 2;
|
||||||
|
expanded.y1 -= 2;
|
||||||
|
expanded.y2 += 2;
|
||||||
|
|
||||||
|
let mut can_build = true;
|
||||||
|
|
||||||
|
for r in map.rooms.iter() {
|
||||||
|
if r.intersect(&rect) { can_build = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in expanded.y1 ..= expanded.y2 {
|
||||||
|
for x in expanded.x1 ..= expanded.x2 {
|
||||||
|
if x > map.width as i32 -2 { can_build = false; }
|
||||||
|
if y > map.height as i32 -2 { can_build = false; }
|
||||||
|
if x < 1 { can_build = false; }
|
||||||
|
if y < 1 { can_build = false; }
|
||||||
|
if can_build {
|
||||||
|
if map.at(x as usize, y as usize) != TileType::Wall {
|
||||||
|
can_build = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
can_build
|
||||||
|
}
|
||||||
|
}
|
|
@ -29,6 +29,7 @@
|
||||||
pub mod map;
|
pub mod map;
|
||||||
pub mod cellular_automata;
|
pub mod cellular_automata;
|
||||||
pub mod cull_unreachable;
|
pub mod cull_unreachable;
|
||||||
|
pub mod bsp_rooms;
|
||||||
pub mod distant_exit;
|
pub mod distant_exit;
|
||||||
pub mod simple_rooms;
|
pub mod simple_rooms;
|
||||||
pub mod rooms_corridors_nearest;
|
pub mod rooms_corridors_nearest;
|
||||||
|
|
Loading…
Reference in New Issue
Block a user