改进的走廊
关于本教程
本教程是免费和开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
到目前为止,我们的走廊生成方式相当原始,存在重叠现象 - 除非您使用 Voronoi 生成,否则走廊里什么都没有。本章将尝试提供更多生成策略(反过来提供更多地图多样性),并允许走廊包含实体。
新的走廊策略:最近邻
使地图感觉更自然的一种方法是在近邻之间构建走廊。这减少(但不能消除)重叠,并且看起来更像是某人可能实际 建造 的东西。我们将创建一个新文件 map_builders/rooms_corridors_nearest.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, Rect, draw_corridor }; use rltk::RandomNumberGenerator; use std::collections::HashSet; pub struct NearestCorridors {} impl MetaMapBuilder for NearestCorridors { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.corridors(rng, build_data); } } impl NearestCorridors { #[allow(dead_code)] pub fn new() -> Box<NearestCorridors> { Box::new(NearestCorridors{}) } fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let rooms : Vec<Rect>; if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { panic!("Nearest Corridors require a builder with room structures"); } let mut connected : HashSet<usize> = HashSet::new(); for (i,room) in rooms.iter().enumerate() { let mut room_distance : Vec<(usize, f32)> = Vec::new(); let room_center = room.center(); let room_center_pt = rltk::Point::new(room_center.0, room_center.1); for (j,other_room) in rooms.iter().enumerate() { if i != j && !connected.contains(&j) { let other_center = other_room.center(); let other_center_pt = rltk::Point::new(other_center.0, other_center.1); let distance = rltk::DistanceAlg::Pythagoras.distance2d( room_center_pt, other_center_pt ); room_distance.push((j, distance)); } } if !room_distance.is_empty() { room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); let dest_center = rooms[room_distance[0].0].center(); draw_corridor( &mut build_data.map, room_center.0, room_center.1, dest_center.0, dest_center.1 ); connected.insert(i); build_data.take_snapshot(); } } } } }
这里有一些您现在应该熟悉的样板代码,所以让我们过一遍 corridors
函数:
- 我们首先获取
rooms
列表,如果没有则panic!
。 - 我们创建一个名为
connected
的新的HashSet
。当房间获得出口时,我们会将房间添加到其中,以避免重复连接到同一个房间。 - 对于每个房间,我们检索一个名为
i
的“枚举”(向量中的索引号)和room
:- 我们创建一个名为
room_distance
的新向量。它存储包含正在考虑的房间的索引和浮点数的元组,该浮点数将存储其到当前房间的距离。 - 我们计算房间的中心,并将其存储在 RLTK 的
Point
中(为了与距离算法兼容)。 - 对于每个房间,我们检索一个名为
j
的枚举(习惯上对计数器使用i
和j
,大概可以追溯到变量名较长很昂贵的日子!),以及other_room
。- 如果
i
和j
相等,我们正在查看通往/来自同一房间的走廊。我们不想这样做,所以我们跳过它! - 同样,如果
other_room
的索引 (j
) 在我们的connected
集合中,那么我们也不想评估它 - 所以我们跳过它。 - 我们计算从外部房间 (
room
/i
) 到我们正在评估的房间 (other_room
/j
) 的距离。 - 我们将距离和
j
索引推送到room_distance
中。
- 如果
- 如果
room_distance
的列表为空,我们向前跳过。否则: - 我们使用
sort_by
对room_distance
向量进行排序,最短距离的排在最前面。 - 然后我们使用
draw_corridor
函数从当前room
的中心绘制到最近的房间(room_distance
中索引为0
的房间)的走廊。
- 我们创建一个名为
最后,我们将修改 map_builders/mod.rs
中的 random_builder
以使用此算法:
#![allow(unused)] fn main() { pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain { /* let mut builder = BuilderChain::new(new_depth); let type_roll = rng.roll_dice(1, 2); match type_roll { 1 => random_room_builder(rng, &mut builder), _ => random_shape_builder(rng, &mut builder) } if rng.roll_dice(1, 3)==1 { builder.with(WaveformCollapseBuilder::new()); } if rng.roll_dice(1, 20)==1 { builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT)); } builder.with(PrefabBuilder::vaults()); builder*/ let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomDrawer::new()); builder.with(RoomSorter::new(RoomSort::LEFTMOST)); builder.with(NearestCorridors::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); builder } }
这提供了连接良好的地图,走廊距离合理地短。如果您 cargo run
该项目,您应该看到类似这样的内容:
.
走廊重叠 仍然 可能发生,但现在已经不太可能了。
带有 Bresenham 线段的走廊
我们可以将走廊绘制为直线,而不是绕过角落呈折线形。这对玩家来说导航起来有点麻烦(需要导航更多角落),但可以产生令人愉悦的效果。我们将创建一个新文件 map_builders/rooms_corridors_lines.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, Rect, TileType }; use rltk::RandomNumberGenerator; use std::collections::HashSet; pub struct StraightLineCorridors {} impl MetaMapBuilder for StraightLineCorridors { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.corridors(rng, build_data); } } impl StraightLineCorridors { #[allow(dead_code)] pub fn new() -> Box<StraightLineCorridors> { Box::new(StraightLineCorridors{}) } fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let rooms : Vec<Rect>; if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { panic!("Straight Line Corridors require a builder with room structures"); } let mut connected : HashSet<usize> = HashSet::new(); for (i,room) in rooms.iter().enumerate() { let mut room_distance : Vec<(usize, f32)> = Vec::new(); let room_center = room.center(); let room_center_pt = rltk::Point::new(room_center.0, room_center.1); for (j,other_room) in rooms.iter().enumerate() { if i != j && !connected.contains(&j) { let other_center = other_room.center(); let other_center_pt = rltk::Point::new(other_center.0, other_center.1); let distance = rltk::DistanceAlg::Pythagoras.distance2d( room_center_pt, other_center_pt ); room_distance.push((j, distance)); } } if !room_distance.is_empty() { room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); let dest_center = rooms[room_distance[0].0].center(); let line = rltk::line2d( rltk::LineAlg::Bresenham, room_center_pt, rltk::Point::new(dest_center.0, dest_center.1) ); for cell in line.iter() { let idx = build_data.map.xy_idx(cell.x, cell.y); build_data.map.tiles[idx] = TileType::Floor; } connected.insert(i); build_data.take_snapshot(); } } } } }
这与前一个几乎相同,但是我们没有调用 draw_corridor
,而是使用 RLTK 的线条函数来绘制从源房间和目标房间中心点的直线。然后,我们将沿着该线段的每个瓦片标记为地板。如果您修改您的 random_builder
以使用此方法:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomDrawer::new()); builder.with(RoomSorter::new(RoomSort::LEFTMOST)); builder.with(StraightLineCorridors::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); builder }
然后 cargo run
您的项目,您将看到类似这样的内容:
.
存储走廊位置
我们将来可能想对我们的走廊位置做些什么,所以让我们存储它们。在 map_builders/mod.rs
中,让我们添加一个容器来存储我们的走廊位置。我们将使其成为一个 Option
,以便保持与不使用此概念的地图类型的兼容性:
#![allow(unused)] fn main() { pub struct BuilderMap { pub spawn_list : Vec<(usize, String)>, pub map : Map, pub starting_position : Option<Position>, pub rooms: Option<Vec<Rect>>, pub corridors: Option<Vec<Vec<usize>>>, pub history : Vec<Map> } }
我们还需要调整构造函数以确保不会忘记 corridors
:
#![allow(unused)] fn main() { impl BuilderChain { pub fn new(new_depth : i32) -> BuilderChain { BuilderChain{ starter: None, builders: Vec::new(), build_data : BuilderMap { spawn_list: Vec::new(), map: Map::new(new_depth), starting_position: None, rooms: None, corridors: None, history : Vec::new() } } } ... }
现在在 common.rs
中,让我们修改我们的走廊函数以返回走廊放置信息:
#![allow(unused)] fn main() { pub fn apply_horizontal_tunnel(map : &mut Map, x1:i32, x2:i32, y:i32) -> Vec<usize> { let mut corridor = Vec::new(); for x in min(x1,x2) ..= max(x1,x2) { let idx = map.xy_idx(x, y); if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor { map.tiles[idx as usize] = TileType::Floor; corridor.push(idx as usize); } } corridor } pub fn apply_vertical_tunnel(map : &mut Map, y1:i32, y2:i32, x:i32) -> Vec<usize> { let mut corridor = Vec::new(); for y in min(y1,y2) ..= max(y1,y2) { let idx = map.xy_idx(x, y); if idx > 0 && idx < map.width as usize * map.height as usize && map.tiles[idx as usize] != TileType::Floor { corridor.push(idx); map.tiles[idx as usize] = TileType::Floor; } } corridor } pub fn draw_corridor(map: &mut Map, x1:i32, y1:i32, x2:i32, y2:i32) -> Vec<usize> { let mut corridor = Vec::new(); let mut x = x1; let mut y = y1; while x != x2 || y != y2 { if x < x2 { x += 1; } else if x > x2 { x -= 1; } else if y < y2 { y += 1; } else if y > y2 { y -= 1; } let idx = map.xy_idx(x, y); if map.tiles[idx] != TileType::Floor { corridor.push(idx); map.tiles[idx] = TileType::Floor; } } corridor } }
请注意,它们基本上没有改变,但现在返回一个瓦片索引向量 - 并且仅在被修改的瓦片是地板时才添加到其中? 这将为我们提供走廊每一段的定义。现在我们需要修改走廊绘制算法以存储此信息。在 rooms_corridors_bsp.rs
中,修改 corridors
函数以执行此操作:
#![allow(unused)] fn main() { ... let mut corridors : Vec<Vec<usize>> = Vec::new(); for i in 0..rooms.len()-1 { let room = rooms[i]; let next_room = 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); let corridor = draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); corridors.push(corridor); build_data.take_snapshot(); } build_data.corridors = Some(corridors); ... }
我们在 rooms_corridors_dogleg.rs
中再次执行相同的操作:
#![allow(unused)] fn main() { ... let mut corridors : Vec<Vec<usize>> = Vec::new(); for (i,room) in rooms.iter().enumerate() { if i > 0 { let (new_x, new_y) = room.center(); let (prev_x, prev_y) = rooms[i as usize -1].center(); if rng.range(0,2) == 1 { let mut c1 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y); let mut c2 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x); c1.append(&mut c2); corridors.push(c1); } else { let mut c1 = apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x); let mut c2 = apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y); c1.append(&mut c2); corridors.push(c1); } build_data.take_snapshot(); } } build_data.corridors = Some(corridors); ... }
您会注意到我们将走廊的第二段附加到第一段,因此我们将其视为一个长走廊,而不是两条走廊。我们需要对我们新创建的 rooms_corridors_lines.rs
应用相同的更改:
#![allow(unused)] fn main() { fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let rooms : Vec<Rect>; if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { panic!("Straight Line Corridors require a builder with room structures"); } let mut connected : HashSet<usize> = HashSet::new(); let mut corridors : Vec<Vec<usize>> = Vec::new(); for (i,room) in rooms.iter().enumerate() { let mut room_distance : Vec<(usize, f32)> = Vec::new(); let room_center = room.center(); let room_center_pt = rltk::Point::new(room_center.0, room_center.1); for (j,other_room) in rooms.iter().enumerate() { if i != j && !connected.contains(&j) { let other_center = other_room.center(); let other_center_pt = rltk::Point::new(other_center.0, other_center.1); let distance = rltk::DistanceAlg::Pythagoras.distance2d( room_center_pt, other_center_pt ); room_distance.push((j, distance)); } } if !room_distance.is_empty() { room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); let dest_center = rooms[room_distance[0].0].center(); let line = rltk::line2d( rltk::LineAlg::Bresenham, room_center_pt, rltk::Point::new(dest_center.0, dest_center.1) ); let mut corridor = Vec::new(); for cell in line.iter() { let idx = build_data.map.xy_idx(cell.x, cell.y); if build_data.map.tiles[idx] != TileType::Floor { build_data.map.tiles[idx] = TileType::Floor; corridor.push(idx); } } corridors.push(corridor); connected.insert(i); build_data.take_snapshot(); } } build_data.corridors = Some(corridors); } }
我们也会在 rooms_corridors_nearest.rs
中做同样的事情:
#![allow(unused)] fn main() { fn corridors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let rooms : Vec<Rect>; if let Some(rooms_builder) = &build_data.rooms { rooms = rooms_builder.clone(); } else { panic!("Nearest Corridors require a builder with room structures"); } let mut connected : HashSet<usize> = HashSet::new(); let mut corridors : Vec<Vec<usize>> = Vec::new(); for (i,room) in rooms.iter().enumerate() { let mut room_distance : Vec<(usize, f32)> = Vec::new(); let room_center = room.center(); let room_center_pt = rltk::Point::new(room_center.0, room_center.1); for (j,other_room) in rooms.iter().enumerate() { if i != j && !connected.contains(&j) { let other_center = other_room.center(); let other_center_pt = rltk::Point::new(other_center.0, other_center.1); let distance = rltk::DistanceAlg::Pythagoras.distance2d( room_center_pt, other_center_pt ); room_distance.push((j, distance)); } } if !room_distance.is_empty() { room_distance.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap() ); let dest_center = rooms[room_distance[0].0].center(); let corridor = draw_corridor( &mut build_data.map, room_center.0, room_center.1, dest_center.0, dest_center.1 ); connected.insert(i); build_data.take_snapshot(); corridors.push(corridor); } } build_data.corridors = Some(corridors); } }
好的,我们有了走廊数据 - 然后呢?
一个明显的用途是在走廊内生成实体。我们将创建新的 room_corridor_spawner.rs
来做到这一点:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, spawner}; use rltk::RandomNumberGenerator; pub struct CorridorSpawner {} impl MetaMapBuilder for CorridorSpawner { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl CorridorSpawner { #[allow(dead_code)] pub fn new() -> Box<CorridorSpawner> { Box::new(CorridorSpawner{}) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(corridors) = &build_data.corridors { for c in corridors.iter() { let depth = build_data.map.depth; spawner::spawn_region(&build_data.map, rng, &c, depth, &mut build_data.spawn_list); } } else { panic!("Corridor Based Spawning only works after corridors have been created"); } } } }
这是基于 room_based_spawner.rs
的 - 复制/粘贴并更改了名称!然后,rooms
的 if let
被替换为 corridors
,并且不是每个房间都生成 - 我们将走廊传递给 spawn_region
。实体现在在走廊中生成。
您可以通过将生成器添加到您的 random_builder
来测试这一点:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomDrawer::new()); builder.with(RoomSorter::new(RoomSort::LEFTMOST)); builder.with(StraightLineCorridors::new()); builder.with(RoomBasedSpawner::new()); builder.with(CorridorSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); builder }
一旦您开始玩游戏,您现在可以在走廊内找到实体:
.
恢复随机性
再一次,这是一个小节的结尾 - 所以我们将再次使 random_builder
随机化,但利用我们的新内容!
首先取消注释 random_builder
中的代码,并删除测试工具:
#![allow(unused)] fn main() { pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain { let mut builder = BuilderChain::new(new_depth); let type_roll = rng.roll_dice(1, 2); match type_roll { 1 => random_room_builder(rng, &mut builder), _ => random_shape_builder(rng, &mut builder) } if rng.roll_dice(1, 3)==1 { builder.with(WaveformCollapseBuilder::new()); } if rng.roll_dice(1, 20)==1 { builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT)); } builder.with(PrefabBuilder::vaults()); builder } }
由于我们在这里所做的一切都是基于 房间 的,我们还将修改 random_room_builder
以包含它。我们将扩展与走廊相关的部分:
#![allow(unused)] fn main() { let corridor_roll = rng.roll_dice(1, 4); match corridor_roll { 1 => builder.with(DoglegCorridors::new()), 2 => builder.with(NearestCorridors::new()), 3 => builder.with(StraightLineCorridors::new()), _ => builder.with(BspCorridors::new()) } let cspawn_roll = rng.roll_dice(1, 2); if cspawn_roll == 1 { builder.with(CorridorSpawner::new()); } }
所以我们添加了直线走廊和最近邻走廊的相等机会,并且 50% 的时间它将在走廊中生成实体。
...
本章的源代码可以在这里找到
使用 WebAssembly 在您的浏览器中运行本章的示例(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.