Voronoi 蜂巢/单元格地图
关于本教程
本教程是免费和开源的,所有代码都使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
我们之前在生成点放置中接触过 Voronoi 图。在本节中,我们将使用它们来制作地图。该算法基本上将地图细分为多个区域,并在它们之间放置墙壁。结果有点像蜂巢。您可以调整距离/邻接算法来调整结果。
脚手架
我们将像之前的章节一样制作脚手架,在 voronoi.rs
中创建 VoronoiBuilder
结构。我们还将调整 random_builder
函数,使其目前只返回 VoronoiBuilder
。
构建 Voronoi 图
在之前的用法中,我们略过了如何实际制作 Voronoi 图 - 并依赖于 rltk
库中的 FastNoise
库。这当然很好,但它并没有真正向我们展示它是 如何 工作的 - 并且提供了非常有限的调整机会。所以 - 我们将自己制作一个。
制作 Voronoi 噪声的第一步是填充一组“种子”。这些是在地图上随机选择(但不是重复)的点。我们将使种子的数量成为一个变量,以便稍后可以对其进行调整。这是代码:
#![allow(unused)] fn main() { let n_seeds = 64; let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); while voronoi_seeds.len() < n_seeds { let vx = rng.roll_dice(1, self.map.width-1); let vy = rng.roll_dice(1, self.map.height-1); let vidx = self.map.xy_idx(vx, vy); let candidate = (vidx, rltk::Point::new(vx, vy)); if !voronoi_seeds.contains(&candidate) { voronoi_seeds.push(candidate); } } }
这创建了一个 vector
,每个条目包含一个 tuple
。在该 tuple 中,我们存储了地图位置的索引,以及包含 x
和 y
坐标的 Point
(如果我们愿意,可以跳过保存这些并从索引计算,但我认为这样更清晰)。然后我们随机确定一个位置,检查以确保我们尚未滚动到该位置,并将其添加。我们重复此过程,直到获得所需数量的种子。64
相当多,但会产生相对密集的蜂巢状结构。
下一步是确定每个单元格的 Voronoi 成员资格:
#![allow(unused)] fn main() { let mut voronoi_distance = vec![(0, 0.0f32) ; n_seeds]; let mut voronoi_membership : Vec<i32> = vec![0 ; self.map.width as usize * self.map.height as usize]; for (i, vid) in voronoi_membership.iter_mut().enumerate() { let x = i as i32 % self.map.width; let y = i as i32 / self.map.width; for (seed, pos) in voronoi_seeds.iter().enumerate() { let distance = rltk::DistanceAlg::PythagorasSquared.distance2d( rltk::Point::new(x, y), pos.1 ); voronoi_distance[seed] = (seed, distance); } voronoi_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); *vid = voronoi_distance[0].0 as i32; } }
在这段代码块中,我们:
- 创建一个新的
vector
,名为voronoi_distance
。它包含usize
和f32
(浮点数) 的元组,并预先创建了n_seeds
个条目。我们可以为每次迭代都创建它,但是重用同一个会快得多。我们将其创建为零。 - 我们创建一个新的
voronoi_membership
向量,其中包含地图上每个瓦片的一个条目。我们将它们全部设置为 0。我们将使用它来存储瓦片属于哪个 Voronoi 单元格。 - 对于
voronoi_membership
中的每个瓦片,我们获得一个枚举器(索引号)和值。我们可变地拥有它,因此我们可以进行更改。- 我们从枚举器 (
i
) 计算瓦片的x
和y
位置。 - 对于
voronoi_seeds
结构中的每个条目,我们获得索引(通过enumerate()
)和位置元组。- 我们使用
PythagorasSquared
算法计算从种子到当前瓦片的距离。 - 我们将
voronoi_distance[seed]
设置为种子索引和距离。
- 我们使用
- 我们按距离对
voronoi_distance
向量进行排序,因此最近的种子将是第一个条目。 - 我们将瓦片的
vid
(Voronoi ID) 设置为voronoi_distance
列表中的第一个条目。
- 我们从枚举器 (
您可以用更简单的英语总结一下:每个瓦片都被赋予了 Voronoi 组的成员资格,该组的种子在物理上离它最近。
接下来,我们使用它来绘制地图:
#![allow(unused)] fn main() { for y in 1..self.map.height-1 { for x in 1..self.map.width-1 { let mut neighbors = 0; let my_idx = self.map.xy_idx(x, y); let my_seed = voronoi_membership[my_idx]; if voronoi_membership[self.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } if neighbors < 2 { self.map.tiles[my_idx] = TileType::Floor; } } self.take_snapshot(); } }
在这段代码中,我们访问除了最外边缘之外的每个瓦片。我们计算有多少相邻的瓦片属于 不同的 Voronoi 组。如果答案是 0,则它完全在该组中:因此我们可以放置地板。如果答案是 1,则它仅与 1 个其他组接壤 - 因此我们也可以放置地板(以确保我们可以在地图上行走)。否则,我们将瓦片保留为墙壁。
然后,我们运行与之前使用过的相同的剔除和放置代码。如果您现在 cargo run
该项目,您将看到一个令人愉悦的结构:
.
调整蜂巢
有两个明显的变量可以暴露给构建器:种子的数量,以及要使用的距离算法。我们将更新结构签名以包含这些:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum DistanceAlgorithm { Pythagoras, Manhattan, Chebyshev } pub struct VoronoiCellBuilder { map : Map, starting_position : Position, depth: i32, history: Vec<Map>, noise_areas : HashMap<i32, Vec<usize>>, n_seeds: usize, distance_algorithm: DistanceAlgorithm } }
然后我们将更新 Voronoi 代码以使用它们:
#![allow(unused)] fn main() { fn build(&mut self) { let mut rng = RandomNumberGenerator::new(); // Make a Voronoi diagram. We'll do this the hard way to learn about the technique! // 制作 Voronoi 图。我们将用比较复杂的方式来做,以了解这项技术! let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); while voronoi_seeds.len() < self.n_seeds { let vx = rng.roll_dice(1, self.map.width-1); let vy = rng.roll_dice(1, self.map.height-1); let vidx = self.map.xy_idx(vx, vy); let candidate = (vidx, rltk::Point::new(vx, vy)); if !voronoi_seeds.contains(&candidate) { voronoi_seeds.push(candidate); } } let mut voronoi_distance = vec![(0, 0.0f32) ; self.n_seeds]; let mut voronoi_membership : Vec<i32> = vec![0 ; self.map.width as usize * self.map.height as usize]; for (i, vid) in voronoi_membership.iter_mut().enumerate() { let x = i as i32 % self.map.width; let y = i as i32 / self.map.width; for (seed, pos) in voronoi_seeds.iter().enumerate() { let distance; match self.distance_algorithm { DistanceAlgorithm::Pythagoras => { distance = rltk::DistanceAlg::PythagorasSquared.distance2d( rltk::Point::new(x, y), pos.1 ); } DistanceAlgorithm::Manhattan => { distance = rltk::DistanceAlg::Manhattan.distance2d( rltk::Point::new(x, y), pos.1 ); } DistanceAlgorithm::Chebyshev => { distance = rltk::DistanceAlg::Chebyshev.distance2d( rltk::Point::new(x, y), pos.1 ); } } voronoi_distance[seed] = (seed, distance); } voronoi_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); *vid = voronoi_distance[0].0 as i32; } for y in 1..self.map.height-1 { for x in 1..self.map.width-1 { let mut neighbors = 0; let my_idx = self.map.xy_idx(x, y); let my_seed = voronoi_membership[my_idx]; if voronoi_membership[self.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } if voronoi_membership[self.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } if neighbors < 2 { self.map.tiles[my_idx] = TileType::Floor; } } self.take_snapshot(); } ... }
作为一个测试,让我们更改构造函数以使用 Manhattan
距离。结果将如下所示:
.
请注意线条是如何更直,更少有机感的。这就是 Manhattan 距离的作用:它像曼哈顿出租车司机一样计算距离 - 行数加列数,而不是直线距离。
恢复随机性
因此,我们将为每种噪声类型放入几个构造函数:
#![allow(unused)] fn main() { pub fn pythagoras(new_depth : i32) -> VoronoiCellBuilder { VoronoiCellBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history: Vec<Map>, noise_areas : HashMap::new(), n_seeds: 64, distance_algorithm: DistanceAlgorithm::Pythagoras } } pub fn manhattan(new_depth : i32) -> VoronoiCellBuilder { VoronoiCellBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history: Vec<Map>, noise_areas : HashMap::new(), n_seeds: 64, distance_algorithm: DistanceAlgorithm::Manhattan } } }
然后我们将恢复 random_builder
,使其再次随机:
#![allow(unused)] fn main() { pub fn random_builder(new_depth: i32) -> Box<dyn MapBuilder> { let mut rng = rltk::RandomNumberGenerator::new(); let builder = rng.roll_dice(1, 16); match builder { 1 => Box::new(BspDungeonBuilder::new(new_depth)), 2 => Box::new(BspInteriorBuilder::new(new_depth)), 3 => Box::new(CellularAutomataBuilder::new(new_depth)), 4 => Box::new(DrunkardsWalkBuilder::open_area(new_depth)), 5 => Box::new(DrunkardsWalkBuilder::open_halls(new_depth)), 6 => Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)), 7 => Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)), 8 => Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)), 9 => Box::new(MazeBuilder::new(new_depth)), 10 => Box::new(DLABuilder::walk_inwards(new_depth)), 11 => Box::new(DLABuilder::walk_outwards(new_depth)), 12 => Box::new(DLABuilder::central_attractor(new_depth)), 13 => Box::new(DLABuilder::insectoid(new_depth)), 14 => Box::new(VoronoiCellBuilder::pythagoras(new_depth)), 15 => Box::new(VoronoiCellBuilder::manhattan(new_depth)), _ => Box::new(SimpleMapBuilder::new(new_depth)) } } }
总结
这又是我们掌握的一种算法!我们现在真的有足够的能力编写一个相当不错的 roguelike 游戏了,但还有更多内容!
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson。