分层/构建器链
关于本教程
本教程是免费和开源的,所有代码都使用 MIT 许可证 - 所以你可以随意使用它。我希望你喜欢这个教程,并制作出伟大的游戏!
如果你喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
在过去的几章中,我们介绍了程序化生成中一个重要的概念:链式构建器。我们很高兴地构建地图,调用波函数坍缩来改变地图,调用我们的 PrefabBuilder
再次改变它,等等。本章将稍微形式化这个过程,扩展它,并为你留下一个框架,让你能够清晰地通过链式连接概念来构建新地图。
基于构建器的接口
构建器链式调用是程序化生成地图的一种非常深刻的方法,它为我们提供了一个机会来清理我们目前构建的很多代码。我们想要一个类似于我们使用 Specs
构建实体的接口:一个构建器,我们可以在其上不断链式调用构建器,并将其作为“执行器”返回 - 准备好构建地图。我们还希望阻止构建器做超过一件事 - 它们应该只做一件事,并把它做好(这是一个好的设计原则;它使调试更容易,并减少重复)。
构建器主要有两种类型:只生成地图的(并且只运行一次才有意义),以及修改现有地图的。我们将分别将它们命名为 InitialMapBuilder
和 MetaMapBuilder
。
这给了我们一个想要采用的语法概念:
- 我们的 Builder 应该有:
- 一个初始构建器 (Initial Builder)。
- n 个元构建器 (Meta Builder),按顺序运行。
那么,构建器应该有一个接受第一个地图的 start_with
方法,以及用于链式连接构建器的额外的 with
方法,这是有道理的。构建器应该存储在一个容器中,该容器保留它们被添加的顺序 - 向量是显而易见的选择。
不再让单个构建器负责设置它们的前置组件也是有道理的;理想情况下,除了 它 所做的事情之外,构建器不应该 必须 知道任何关于过程的事情。因此,我们需要抽象这个过程,并支持快照(以便您可以查看程序化生成过程)。
共享地图状态 - BuilderMap
与其让每个构建器定义它们自己的共享数据副本,不如将共享数据放在一个地方 - 并在需要时在链中传递它,这将更有意义。因此,我们将首先在 map_builders/mod.rs
中定义一些新的结构和接口。首先,我们将创建 BuilderMap
:
#![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 history : Vec<Map> } }
你会注意到,这包含了我们一直在构建到每个地图构建器中的所有数据 - 仅此而已。它是有意通用的 - 我们将把它传递给构建器,并让他们处理它。请注意,所有字段都是 公共的 - 这是因为我们正在传递它,并且很有可能任何接触它的东西都需要访问它的全部或部分内容。
BuilderMap
还需要方便进行快照的任务,以便调试器查看我们处理算法时的地图。我们将在 BuilderMap
中放入一个函数 - 用于处理快照开发:
#![allow(unused)] fn main() { impl BuilderMap { 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); } } } }
这与我们一直在混入构建器的 take_snapshot
代码 相同。由于我们正在使用地图构建知识的中央存储库,我们可以提升它以应用于 所有 我们的构建器。
BuilderChain
- 管理地图创建的主构建器
以前,我们传递了 MapBuilder
类,每个类都能够构建之前的地图。由于我们已经得出结论,这是一个糟糕的想法,并定义了我们 想要 的语法,我们将创建一个替代品。BuilderChain
是一个 主 构建器 - 它控制整个构建过程。为此,我们将添加 BuilderChain
类型:
#![allow(unused)] fn main() { pub struct BuilderChain { starter: Option<Box<dyn InitialMapBuilder>>, builders: Vec<Box<dyn MetaMapBuilder>>, pub build_data : BuilderMap } }
这是一个更复杂的结构,所以让我们过一遍:
starter
是一个Option
,所以我们知道是否存在一个。没有第一步(不引用其他地图的地图)将是一个错误条件,因此我们将跟踪它。我们正在引用一个新的 trait,InitialMapBuilder
;我们稍后会介绍它。builders
是MetaMapBuilders
的向量,另一个新的 trait(同样 - 我们稍后会介绍它)。这些是操作先前地图结果的构建器。build_data
是一个公共变量(任何人都可以读/写它),包含我们刚刚创建的BuilderMap
。
我们将实现一些函数来支持它。首先,一个 构造函数:
#![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, history : Vec::new() } } } ... }
这非常简单:它创建了一个新的 BuilderChain
,所有内容都使用默认值。现在,让我们允许我们的用户向链中添加 起始地图。(起始地图是不需要先前地图作为输入的第一步,并生成可用的地图结构,我们可以对其进行修改):
#![allow(unused)] fn main() { ... pub fn start_with(&mut self, starter : Box<dyn InitialMapBuilder>) { match self.starter { None => self.starter = Some(starter), Some(_) => panic!("You can only have one starting builder.") }; } ... }
这里有一个新概念:panic!
。如果用户尝试添加第二个起始构建器,我们将崩溃 - 因为这没有任何意义。你只是简单地覆盖你之前的步骤,这是一个巨大的时间浪费!我们还将允许用户添加元构建器:
#![allow(unused)] fn main() { ... pub fn with(&mut self, metabuilder : Box<dyn MetaMapBuilder>) { self.builders.push(metabuilder); } ... }
这非常简单:我们只需将元构建器添加到构建器向量中。由于向量保持您添加到它们的顺序,因此您的操作将保持适当的排序。最后,我们将实现一个实际构建地图的函数:
#![allow(unused)] fn main() { pub fn build_map(&mut self, rng : &mut rltk::RandomNumberGenerator) { match &mut self.starter { None => panic!("Cannot run a map builder chain without a starting build system"), Some(starter) => { // 构建起始地图 starter.build_map(rng, &mut self.build_data); } } // 依次构建额外的层 for metabuilder in self.builders.iter_mut() { metabuilder.build_map(rng, &mut self.build_data); } } }
让我们在这里逐步了解一下:
- 我们
match
了我们的起始地图。如果没有起始地图,我们会 panic - 并崩溃程序,并显示一条消息,提示您 必须 设置一个起始构建器。 - 我们在起始地图上调用
build_map
。 - 对于每个元构建器,我们在其上调用
build_map
- 按照指定的顺序。
这不是一个糟糕的语法!它应该使我们能够将构建器链接在一起,并为构建复杂的分层地图提供所需的概览。
新的 Traits - InitialMapBuilder
和 MetaMapBuilder
让我们看一下我们定义的两个 trait 接口,InitialMapBuilder
和 MetaMapBuilder
。我们将它们设为单独的类型,以强制用户只选择 一个 起始构建器,而不是尝试将任何起始构建器放入修改层列表中。它们的实现是相同的:
#![allow(unused)] fn main() { pub trait InitialMapBuilder { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap); } pub trait MetaMapBuilder { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap); } }
build_map
接受一个随机数生成器(这样我们就不会到处创建新的生成器了!),以及我们正在处理的 BuilderMap
的可变引用。因此,我们不是让每个构建器可选地调用前一个构建器,而是在处理状态时传递状态。
生成函数
我们还需要实现我们的生成系统:
#![allow(unused)] fn main() { pub fn spawn_entities(&mut self, ecs : &mut World) { for entity in self.build_data.spawn_list.iter() { spawner::spawn_entity(ecs, &(&entity.0, &entity.1)); } } }
这几乎与我们之前在 MapBuilder
中的生成器代码相同,但我们是从 build_data
结构中的 spawn_list
中生成的。否则,它是相同的。
随机构建器 - 第一步
最后,我们将修改 random_builder
以使用我们的 SimpleMapBuilder
和一些新类型来分解创建步骤:
#![allow(unused)] fn main() { pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain { let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStartingPosition::new()); builder.with(RoomBasedStairs::new()); builder } }
请注意,我们现在正在使用 RandomNumberGenerator
参数。这是因为我们想使用全局 RNG,而不是一直创建新的 RNG。这样,如果调用者设置了“种子” - 它将应用于世界生成。这计划成为未来章节的主题。我们现在还返回 BuilderChain
而不是 boxed trait - 我们将 messy boxing/dynamic dispatch 隐藏在实现内部,因此调用者不必担心它。这里还有两个新类型:RoomBasedSpawner
和 RoomBasedStartingPosition
- 以及 SimpleMapBuilder
的更改后的构造函数(它不再接受深度参数)。我们稍后会介绍这一点 - 但首先,让我们处理由于新接口而导致的主程序更改。
看起来不错的界面 - 但你破坏了东西!
我们现在拥有了我们想要的 接口 - 系统如何与世界交互的良好地图。不幸的是,世界仍然期望我们之前的设置 - 所以我们需要修复它。在 main.rs
中,我们需要更新我们的 generate_world_map
函数以使用新接口:
#![allow(unused)] fn main() { fn generate_world_map(&mut self, new_depth : i32) { self.mapgen_index = 0; self.mapgen_timer = 0.0; self.mapgen_history.clear(); let mut rng = self.ecs.write_resource::<rltk::RandomNumberGenerator>(); let mut builder = map_builders::random_builder(new_depth, &mut rng); builder.build_map(&mut rng); std::mem::drop(rng); self.mapgen_history = builder.build_data.history.clone(); let player_start; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); *worldmap_resource = builder.build_data.map.clone(); player_start = builder.build_data.starting_position.as_mut().unwrap().clone(); } // 生成坏人 builder.spawn_entities(&mut self.ecs); }
- 我们重置
mapgen_index
、mapgen_timer
和mapgen_history
,以便进度查看器从头开始运行。 - 我们从 ECS
World
获取 RNG。 - 我们使用新接口创建一个新的
random_builder
,并传递随机数生成器。 - 我们告诉它从链中构建新地图,也利用 RNG。
- 我们在 RNG 上调用
std::mem::drop
。这停止了对它的“借用” - 因此我们也不再借用self
。这防止了代码的后续阶段出现借用检查器错误。 - 我们将地图构建器历史记录 克隆 到我们自己的世界历史记录副本中。我们复制它,这样我们就不会破坏构建器。
- 我们将
player_start
设置为构建器确定的起始位置的 克隆。请注意,我们正在调用unwrap
- 因此起始位置的Option
必须 在此时有一个值,否则我们将崩溃。这是故意的:我们宁愿崩溃,知道我们忘记设置起始点,也不愿程序在未知/混乱的状态下运行。 - 我们调用
spawn_entities
来填充地图。
修改 SimpleMapBuilder
我们可以大大简化 SimpleMapBuilder
(使其名副其实!)。这是新代码:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, Rect, apply_room_to_map, apply_horizontal_tunnel, apply_vertical_tunnel }; use rltk::RandomNumberGenerator; pub struct SimpleMapBuilder {} impl InitialMapBuilder for SimpleMapBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.rooms_and_corridors(rng, build_data); } } impl SimpleMapBuilder { #[allow(dead_code)] pub fn new() -> Box<SimpleMapBuilder> { Box::new(SimpleMapBuilder{}) } fn rooms_and_corridors(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { const MAX_ROOMS : i32 = 30; const MIN_SIZE : i32 = 6; const MAX_SIZE : i32 = 10; let mut rooms : Vec<Rect> = Vec::new(); for i in 0..MAX_ROOMS { let w = rng.range(MIN_SIZE, MAX_SIZE); let h = rng.range(MIN_SIZE, MAX_SIZE); let x = rng.roll_dice(1, build_data.map.width - w - 1) - 1; let y = rng.roll_dice(1, build_data.map.height - h - 1) - 1; let new_room = Rect::new(x, y, w, h); let mut ok = true; for other_room in rooms.iter() { if new_room.intersect(other_room) { ok = false } } if ok { apply_room_to_map(&mut build_data.map, &new_room); build_data.take_snapshot(); if !rooms.is_empty() { let (new_x, new_y) = new_room.center(); let (prev_x, prev_y) = rooms[i as usize -1].center(); if rng.range(0,2) == 1 { apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, prev_y); apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, new_x); } else { apply_vertical_tunnel(&mut build_data.map, prev_y, new_y, prev_x); apply_horizontal_tunnel(&mut build_data.map, prev_x, new_x, new_y); } } rooms.push(new_room); build_data.take_snapshot(); } } build_data.rooms = Some(rooms); } } }
这基本上与旧的 SimpleMapBuilder
相同,但有一些更改:
- 请注意,我们只应用了
InitialMapBuilder
trait -MapBuilder
不复存在了。 - 我们也没有设置起始位置或生成实体 - 这些现在是链中其他构建器的职权范围。我们基本上将其提炼为仅房间构建算法。
- 我们将
build_data.rooms
设置为Some(rooms)
。并非所有算法都支持房间 - 因此我们的 trait 将Option
设置为None
,直到我们填充它为止。由于SimpleMapBuilder
完全是关于房间的 - 我们填充它。
基于房间的生成
在 map_builders
目录中创建一个新文件 room_based_spawner.rs
。我们将在这里应用旧 SimpleMapBuilder
中的 仅 房间填充系统:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, spawner}; use rltk::RandomNumberGenerator; pub struct RoomBasedSpawner {} impl MetaMapBuilder for RoomBasedSpawner { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl RoomBasedSpawner { #[allow(dead_code)] pub fn new() -> Box<RoomBasedSpawner> { Box::new(RoomBasedSpawner{}) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(rooms) = &build_data.rooms { for room in rooms.iter().skip(1) { spawner::spawn_room(&build_data.map, rng, room, build_data.map.depth, &mut build_data.spawn_list); } } else { panic!("Room Based Spawning only works after rooms have been created"); } } } }
在这个子模块中,我们正在实现 MetaMapBuilder
:这个构建器要求您已经有一个地图。在 build
中,我们从 SimpleMapBuilder
中复制了旧的基于房间的生成代码,并对其进行了修改以在构建器的 rooms
结构上运行。为此,我们使用 if let
来获取 Option
的内部值;如果没有,那么我们 panic!
,程序退出,并声明基于房间的生成仅在您 有 房间的情况下才有效。
我们将功能减少到仅一个任务:如果有房间,我们在其中生成怪物。
基于房间的起始位置
这与基于房间的生成非常相似,但将玩家放置在第一个房间中 - 就像以前在 SimpleMapBuilder
中一样。在 map_builders
中创建一个新文件 room_based_starting_position
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, Position}; use rltk::RandomNumberGenerator; pub struct RoomBasedStartingPosition {} impl MetaMapBuilder for RoomBasedStartingPosition { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl RoomBasedStartingPosition { #[allow(dead_code)] pub fn new() -> Box<RoomBasedStartingPosition> { Box::new(RoomBasedStartingPosition{}) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(rooms) = &build_data.rooms { let start_pos = rooms[0].center(); build_data.starting_position = Some(Position{ x: start_pos.0, y: start_pos.1 }); } else { panic!("Room Based Staring Position only works after rooms have been created"); } } } }
基于房间的楼梯
这也非常类似于我们在 SimpleMapBuilder
中生成出口楼梯的方式。创建一个新文件 room_based_stairs.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; pub struct RoomBasedStairs {} impl MetaMapBuilder for RoomBasedStairs { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl RoomBasedStairs { #[allow(dead_code)] pub fn new() -> Box<RoomBasedStairs> { Box::new(RoomBasedStairs{}) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(rooms) = &build_data.rooms { let stairs_position = rooms[rooms.len()-1].center(); let stairs_idx = build_data.map.xy_idx(stairs_position.0, stairs_position.1); build_data.map.tiles[stairs_idx] = TileType::DownStairs; build_data.take_snapshot(); } else { panic!("Room Based Stairs only works after rooms have been created"); } } } }
将它们放在一起,用新框架制作一个简单的地图
让我们再次看一下 random_builder
:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStartingPosition::new()); builder.with(RoomBasedStairs::new()); builder }
既然我们已经完成了所有步骤,这应该是有意义的:
- 我们 从 使用
SimpleMapBuilder
生成器生成的地图 开始。 - 我们使用 元构建器
RoomBasedSpawner
修改 地图,以在房间中生成实体。 - 我们再次使用 元构建器
RoomBasedStartingPosition
修改 地图,以便从第一个房间开始。 - 再次,我们使用 元构建器
RoomBasedStairs
修改 地图,以在最后一个房间中放置向下楼梯。
如果你现在 cargo run
项目,你将看到很多关于未使用代码的警告 - 但游戏应该可以玩,只有我们第一节中的简单地图。您可能想知道 为什么 我们付出了这么多努力来保持事物相同;希望随着我们清理更多构建器,这一点会变得清晰!
清理 BSP 地牢构建器
再次,我们可以认真清理地图构建器!这是新版本的 bsp_dungeon.rs
:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, Map, Rect, apply_room_to_map, TileType, draw_corridor}; use rltk::RandomNumberGenerator; pub struct BspDungeonBuilder { rects: Vec<Rect>, } impl InitialMapBuilder for BspDungeonBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl BspDungeonBuilder { #[allow(dead_code)] pub fn new() -> Box<BspDungeonBuilder> { Box::new(BspDungeonBuilder{ rects: Vec::new(), }) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let mut rooms : Vec<Rect> = Vec::new(); self.rects.clear(); self.rects.push( Rect::new(2, 2, build_data.map.width-5, build_data.map.height-5) ); // 从单个地图大小的矩形开始 let first_room = self.rects[0]; self.add_subrects(first_room); // 划分第一个房间 // 最多 240 次,我们获得一个随机矩形并划分它。如果有可能在那里挤出一个房间, // 我们放置它并将其添加到房间列表。 let mut n_rooms = 0; while n_rooms < 240 { let rect = self.get_random_rect(rng); let candidate = self.get_random_sub_rect(rect, rng); if self.is_possible(candidate, &build_data.map) { apply_room_to_map(&mut build_data.map, &candidate); rooms.push(candidate); self.add_subrects(rect); build_data.take_snapshot(); } n_rooms += 1; } // 现在我们对房间进行排序 rooms.sort_by(|a,b| a.x1.cmp(&b.x1) ); // 现在我们需要走廊 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); draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); build_data.take_snapshot(); } build_data.rooms = Some(rooms); } fn add_subrects(&mut self, rect : Rect) { 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); self.rects.push(Rect::new( rect.x1, rect.y1, half_width, half_height )); self.rects.push(Rect::new( rect.x1, rect.y1 + half_height, half_width, half_height )); self.rects.push(Rect::new( rect.x1 + half_width, rect.y1, half_width, half_height )); self.rects.push(Rect::new( rect.x1 + half_width, rect.y1 + half_height, half_width, half_height )); } fn get_random_rect(&mut self, rng : &mut RandomNumberGenerator) -> Rect { if self.rects.len() == 1 { return self.rects[0]; } let idx = (rng.roll_dice(1, self.rects.len() as i32)-1) as usize; self.rects[idx] } fn get_random_sub_rect(&self, rect : Rect, rng : &mut RandomNumberGenerator) -> 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 = i32::max(3, rng.roll_dice(1, i32::min(rect_width, 10))-1) + 1; let h = i32::max(3, rng.roll_dice(1, i32::min(rect_height, 10))-1) + 1; result.x1 += rng.roll_dice(1, 6)-1; result.y1 += rng.roll_dice(1, 6)-1; result.x2 = result.x1 + w; result.y2 = result.y1 + h; 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 y in expanded.y1 ..= expanded.y2 { for x in expanded.x1 ..= expanded.x2 { if x > map.width-2 { can_build = false; } if y > map.height-2 { can_build = false; } if x < 1 { can_build = false; } if y < 1 { can_build = false; } if can_build { let idx = map.xy_idx(x, y); if map.tiles[idx] != TileType::Wall { can_build = false; } } } } can_build } } }
就像 SimpleMapBuilder
一样,我们已经剥离了所有非房间构建代码,使其成为更简洁的代码。我们正在引用构建器的 build_data
结构,而不是制作我们自己的所有内容的副本 - 代码的 核心 部分在很大程度上是相同的。
现在您可以修改 random_builder
以制作此地图类型:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(BspDungeonBuilder::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStartingPosition::new()); builder.with(RoomBasedStairs::new()); builder }
如果你现在 cargo run
,你将获得一个基于 BspDungeonBuilder
的地牢。看看你是如何重用生成器、起始位置和楼梯代码的?这绝对比旧版本有所改进 - 如果您更改一个,它现在可以帮助多个构建器!
再次针对 BSP 内部
再次,我们可以极大地清理构建器 - 这次是 BspInteriorBuilder
。这是 bsp_interior.rs
的代码:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, Rect, TileType, draw_corridor}; use rltk::RandomNumberGenerator; const MIN_ROOM_SIZE : i32 = 8; pub struct BspInteriorBuilder { rects: Vec<Rect> } impl InitialMapBuilder for BspInteriorBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl BspInteriorBuilder { #[allow(dead_code)] pub fn new() -> Box<BspInteriorBuilder> { Box::new(BspInteriorBuilder{ rects: Vec::new() }) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let mut rooms : Vec<Rect> = Vec::new(); self.rects.clear(); self.rects.push( Rect::new(1, 1, build_data.map.width-2, build_data.map.height-2) ); // 从单个地图大小的矩形开始 let first_room = self.rects[0]; self.add_subrects(first_room, rng); // 划分第一个房间 let rooms_copy = self.rects.clone(); for r in rooms_copy.iter() { let room = *r; //room.x2 -= 1; //room.y2 -= 1; rooms.push(room); for y in room.y1 .. room.y2 { for x in room.x1 .. room.x2 { let idx = build_data.map.xy_idx(x, y); if idx > 0 && idx < ((build_data.map.width * build_data.map.height)-1) as usize { build_data.map.tiles[idx] = TileType::Floor; } } } build_data.take_snapshot(); } // 现在我们需要走廊 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); draw_corridor(&mut build_data.map, start_x, start_y, end_x, end_y); build_data.take_snapshot(); } build_data.rooms = Some(rooms); } fn add_subrects(&mut self, rect : Rect, rng : &mut RandomNumberGenerator) { // 从列表中删除最后一个矩形 if !self.rects.is_empty() { self.rects.remove(self.rects.len() - 1); } // 计算边界 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 { // 水平分割 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 { // 垂直分割 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); } } } } }
您可以通过修改 random_builder
来测试它:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(BspInteriorBuilder::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStartingPosition::new()); builder.with(RoomBasedStairs::new()); builder }
cargo run
现在将带您进入一个内部构建器。
细胞自动机
您现在应该理解这里的基本思想了 - 我们正在将构建器分解为小块,并为地图类型实现适当的 traits。查看细胞自动机地图,您会发现我们做事的方式略有不同:
- 我们像往常一样制作地图。这显然属于
CellularAutomataBuilder
。 - 我们搜索靠近中间的起始点。这看起来应该是一个单独的步骤。
- 我们搜索地图中无法到达的区域并剔除它们。这看起来也是一个单独的步骤。
- 我们将出口放置在远离起始位置的地方。这也是一个不同的算法步骤。
好消息是,其中最后三个步骤在许多其他构建器中使用 - 因此实现它们将使我们能够重用代码,而不会不断重复自己。坏消息是,如果我们使用现有的基于房间的步骤运行我们的细胞自动机构建器,它将崩溃 - 我们没有 房间!
因此,我们将从构建基本的地图构建器开始。像其他构建器一样,这主要只是重新排列代码以适应新的 trait 方案。这是新的 cellular_automata.rs
文件:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; pub struct CellularAutomataBuilder {} impl InitialMapBuilder for CellularAutomataBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl CellularAutomataBuilder { #[allow(dead_code)] pub fn new() -> Box<CellularAutomataBuilder> { Box::new(CellularAutomataBuilder{}) } #[allow(clippy::map_entry)] fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 首先,我们完全随机化地图,将其 55% 设置为地板。 for y in 1..build_data.map.height-1 { for x in 1..build_data.map.width-1 { let roll = rng.roll_dice(1, 100); let idx = build_data.map.xy_idx(x, y); if roll > 55 { build_data.map.tiles[idx] = TileType::Floor } else { build_data.map.tiles[idx] = TileType::Wall } } } build_data.take_snapshot(); // 现在我们迭代地应用细胞自动机规则 for _i in 0..15 { let mut newtiles = build_data.map.tiles.clone(); for y in 1..build_data.map.height-1 { for x in 1..build_data.map.width-1 { let idx = build_data.map.xy_idx(x, y); let mut neighbors = 0; if build_data.map.tiles[idx - 1] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx + 1] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx - (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx - (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx + (build_data.map.width as usize - 1)] == TileType::Wall { neighbors += 1; } if build_data.map.tiles[idx + (build_data.map.width as usize + 1)] == TileType::Wall { neighbors += 1; } if neighbors > 4 || neighbors == 0 { newtiles[idx] = TileType::Wall; } else { newtiles[idx] = TileType::Floor; } } } build_data.map.tiles = newtiles.clone(); build_data.take_snapshot(); } } } }
非房间起始点
我们完全有可能实际上并不 想 从地图中间开始。这样做提供了很多机会(并有助于确保连通性),但也许您宁愿玩家跋涉穿过很多地图,而减少选择错误方向的机会。如果玩家到达地图的一端并从另一端离开,也许您的故事更有意义。让我们实现一个起始位置系统,该系统采用 首选的 起始点,并选择最近的有效瓦片。创建 area_starting_points.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, Position, TileType}; use rltk::RandomNumberGenerator; #[allow(dead_code)] pub enum XStart { LEFT, CENTER, RIGHT } #[allow(dead_code)] pub enum YStart { TOP, CENTER, BOTTOM } pub struct AreaStartingPosition { x : XStart, y : YStart } impl MetaMapBuilder for AreaStartingPosition { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl AreaStartingPosition { #[allow(dead_code)] pub fn new(x : XStart, y : YStart) -> Box<AreaStartingPosition> { Box::new(AreaStartingPosition{ x, y }) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let seed_x; let seed_y; match self.x { XStart::LEFT => seed_x = 1, XStart::CENTER => seed_x = build_data.map.width / 2, XStart::RIGHT => seed_x = build_data.map.width - 2 } match self.y { YStart::TOP => seed_y = 1, YStart::CENTER => seed_y = build_data.map.height / 2, YStart::BOTTOM => seed_y = build_data.map.height - 2 } let mut available_floors : Vec<(usize, f32)> = Vec::new(); for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { if *tiletype == TileType::Floor { available_floors.push( ( idx, rltk::DistanceAlg::PythagorasSquared.distance2d( rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), rltk::Point::new(seed_x, seed_y) ) ) ); } } if available_floors.is_empty() { panic!("No valid floors to start on"); } available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); let start_x = available_floors[0].0 as i32 % build_data.map.width; let start_y = available_floors[0].0 as i32 / build_data.map.width; build_data.starting_position = Some(Position{x : start_x, y: start_y}); } } }
我们已经介绍了足够的样板代码,不需要再次介绍了 - 所以让我们逐步了解 build 函数:
- 我们接受几个
enum
类型:X 轴和 Y 轴上的首选位置。 - 因此,我们将
seed_x
和seed_y
设置为最接近指定位置的点。 - 我们遍历整个地图,将地板瓦片添加到
available_floors
- 并计算到首选起始点的距离。 - 我们对可用瓦片列表进行排序,以便距离较小的瓦片排在前面。
- 我们选择列表中的第一个。
请注意,如果没有地板,我们也会 panic!
。
这里最棒的部分是,这将适用于 任何 地图类型 - 它搜索可以站立的地板,并尝试找到最近的起始点。
剔除无法到达的区域
我们之前在剔除无法从起始点到达的区域方面取得了不错的成功。因此,让我们将其形式化为自己的元构建器。创建 cull_unreachable.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; pub struct CullUnreachable {} impl MetaMapBuilder for CullUnreachable { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl CullUnreachable { #[allow(dead_code)] pub fn new() -> Box<CullUnreachable> { Box::new(CullUnreachable{}) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); let start_idx = build_data.map.xy_idx( starting_pos.x, starting_pos.y ); build_data.map.populate_blocked(); let map_starts : Vec<usize> = vec![start_idx]; let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width as usize, build_data.map.height as usize, &map_starts , &build_data.map, 1000.0); for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { if *tile == TileType::Floor { let distance_to_start = dijkstra_map.map[i]; // 我们无法到达此瓦片 - 因此我们将其设为墙壁 if distance_to_start == std::f32::MAX { *tile = TileType::Wall; } } } } } }
您会注意到这几乎与 common.rs
中的 remove_unreachable_areas_returning_most_distant
相同,但没有返回 Dijkstra 地图。这就是意图:我们删除玩家无法到达的区域,并且 只 做这件事。
基于 Voronoi 的生成
我们还需要复制基于 Voronoi 的生成的功能。创建 voronoi_spawning.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, TileType, spawner}; use rltk::RandomNumberGenerator; use std::collections::HashMap; pub struct VoronoiSpawning {} impl MetaMapBuilder for VoronoiSpawning { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl VoronoiSpawning { #[allow(dead_code)] pub fn new() -> Box<VoronoiSpawning> { Box::new(VoronoiSpawning{}) } #[allow(clippy::map_entry)] fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let mut noise_areas : HashMap<i32, Vec<usize>> = HashMap::new(); let mut noise = rltk::FastNoise::seeded(rng.roll_dice(1, 65536) as u64); noise.set_noise_type(rltk::NoiseType::Cellular); noise.set_frequency(0.08); noise.set_cellular_distance_function(rltk::CellularDistanceFunction::Manhattan); for y in 1 .. build_data.map.height-1 { for x in 1 .. build_data.map.width-1 { let idx = build_data.map.xy_idx(x, y); if build_data.map.tiles[idx] == TileType::Floor { let cell_value_f = noise.get_noise(x as f32, y as f32) * 10240.0; let cell_value = cell_value_f as i32; if noise_areas.contains_key(&cell_value) { noise_areas.get_mut(&cell_value).unwrap().push(idx); } else { noise_areas.insert(cell_value, vec![idx]); } } } } // 生成实体 for area in noise_areas.iter() { spawner::spawn_region(&build_data.map, rng, area.1, build_data.map.depth, &mut build_data.spawn_list); } } } }
这几乎与我们在各种构建器中调用的 common.rs
中的代码相同,只是进行了修改以在构建器链/构建器地图框架内工作。
生成一个遥远的出口
另一个常用的代码片段生成了关卡的 Dijkstra 地图,从玩家的入口点开始 - 并使用该地图将出口放置在离玩家最远的位置。这在 common.rs
中,我们经常调用它。我们将把这个变成地图构建步骤;创建 map_builders/distant_exit.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; pub struct DistantExit {} impl MetaMapBuilder for DistantExit { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl DistantExit { #[allow(dead_code)] pub fn new() -> Box<DistantExit> { Box::new(DistantExit{}) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { let starting_pos = build_data.starting_position.as_ref().unwrap().clone(); let start_idx = build_data.map.xy_idx( starting_pos.x, starting_pos.y ); build_data.map.populate_blocked(); let map_starts : Vec<usize> = vec![start_idx]; let dijkstra_map = rltk::DijkstraMap::new(build_data.map.width as usize, build_data.map.height as usize, &map_starts , &build_data.map, 1000.0); let mut exit_tile = (0, 0.0f32); for (i, tile) in build_data.map.tiles.iter_mut().enumerate() { if *tile == TileType::Floor { let distance_to_start = dijkstra_map.map[i]; if distance_to_start != std::f32::MAX { // 如果它比我们当前的出口候选更远,则移动出口 if distance_to_start > exit_tile.1 { exit_tile.0 = i; exit_tile.1 = distance_to_start; } } } } // 放置楼梯 let stairs_idx = exit_tile.0; build_data.map.tiles[stairs_idx] = TileType::DownStairs; build_data.take_snapshot(); } } }
同样,这是我们之前使用过的相同代码 - 只是进行了调整以匹配新接口,因此我们不会详细介绍。
测试细胞自动机
我们终于把所有部件都放在一起了,让我们测试一下。在 random_builder
中,我们将使用新的构建器链:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(CellularAutomataBuilder::new()); builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); builder.with(CullUnreachable::new()); builder.with(VoronoiSpawning::new()); builder.with(DistantExit::new()); builder }
如果您现在 cargo run
,您将可以在细胞自动机生成的地图中玩游戏。
更新醉汉漫步
您应该对我们现在正在做的事情有一个很好的了解,因此我们将略过对 drunkard.rs
的更改:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, TileType, Position, paint, Symmetry}; use rltk::RandomNumberGenerator; #[derive(PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum DrunkSpawnMode { StartingPoint, Random } pub struct DrunkardSettings { pub spawn_mode : DrunkSpawnMode, pub drunken_lifetime : i32, pub floor_percent: f32, pub brush_size: i32, pub symmetry: Symmetry } pub struct DrunkardsWalkBuilder { settings : DrunkardSettings } impl InitialMapBuilder for DrunkardsWalkBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl DrunkardsWalkBuilder { #[allow(dead_code)] pub fn new(settings: DrunkardSettings) -> DrunkardsWalkBuilder { DrunkardsWalkBuilder{ settings } } #[allow(dead_code)] pub fn open_area() -> Box<DrunkardsWalkBuilder> { Box::new(DrunkardsWalkBuilder{ settings : DrunkardSettings{ spawn_mode: DrunkSpawnMode::StartingPoint, drunken_lifetime: 400, floor_percent: 0.5, brush_size: 1, symmetry: Symmetry::None } }) } #[allow(dead_code)] pub fn open_halls() -> Box<DrunkardsWalkBuilder> { Box::new(DrunkardsWalkBuilder{ settings : DrunkardSettings{ spawn_mode: DrunkSpawnMode::Random, drunken_lifetime: 400, floor_percent: 0.5, brush_size: 1, symmetry: Symmetry::None }, }) } #[allow(dead_code)] pub fn winding_passages() -> Box<DrunkardsWalkBuilder> { Box::new(DrunkardsWalkBuilder{ settings : DrunkardSettings{ spawn_mode: DrunkSpawnMode::Random, drunken_lifetime: 100, floor_percent: 0.4, brush_size: 1, symmetry: Symmetry::None }, }) } #[allow(dead_code)] pub fn fat_passages() -> Box<DrunkardsWalkBuilder> { Box::new(DrunkardsWalkBuilder{ settings : DrunkardSettings{ spawn_mode: DrunkSpawnMode::Random, drunken_lifetime: 100, floor_percent: 0.4, brush_size: 2, symmetry: Symmetry::None }, }) } #[allow(dead_code)] pub fn fearful_symmetry() -> Box<DrunkardsWalkBuilder> { Box::new(DrunkardsWalkBuilder{ settings : DrunkardSettings{ spawn_mode: DrunkSpawnMode::Random, drunken_lifetime: 100, floor_percent: 0.4, brush_size: 1, symmetry: Symmetry::Both }, }) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 设置中心起始点 let starting_position = Position{ x: build_data.map.width / 2, y: build_data.map.height / 2 }; let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y); build_data.map.tiles[start_idx] = TileType::Floor; let total_tiles = build_data.map.width * build_data.map.height; let desired_floor_tiles = (self.settings.floor_percent * total_tiles as f32) as usize; let mut floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); let mut digger_count = 0; while floor_tile_count < desired_floor_tiles { let mut did_something = false; let mut drunk_x; let mut drunk_y; match self.settings.spawn_mode { DrunkSpawnMode::StartingPoint => { drunk_x = starting_position.x; drunk_y = starting_position.y; } DrunkSpawnMode::Random => { if digger_count == 0 { drunk_x = starting_position.x; drunk_y = starting_position.y; } else { drunk_x = rng.roll_dice(1, build_data.map.width - 3) + 1; drunk_y = rng.roll_dice(1, build_data.map.height - 3) + 1; } } } let mut drunk_life = self.settings.drunken_lifetime; while drunk_life > 0 { let drunk_idx = build_data.map.xy_idx(drunk_x, drunk_y); if build_data.map.tiles[drunk_idx] == TileType::Wall { did_something = true; } paint(&mut build_data.map, self.settings.symmetry, self.settings.brush_size, drunk_x, drunk_y); build_data.map.tiles[drunk_idx] = TileType::DownStairs; let stagger_direction = rng.roll_dice(1, 4); match stagger_direction { 1 => { if drunk_x > 2 { drunk_x -= 1; } } 2 => { if drunk_x < build_data.map.width-2 { drunk_x += 1; } } 3 => { if drunk_y > 2 { drunk_y -=1; } } _ => { if drunk_y < build_data.map.height-2 { drunk_y += 1; } } } drunk_life -= 1; } if did_something { build_data.take_snapshot(); } digger_count += 1; for t in build_data.map.tiles.iter_mut() { if *t == TileType::DownStairs { *t = TileType::Floor; } } floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); } } } }
再次,您可以通过调整 random_builder
来测试它:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(DrunkardsWalkBuilder::fearful_symmetry()); builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); builder.with(CullUnreachable::new()); builder.with(VoronoiSpawning::new()); builder.with(DistantExit::new()); builder }
您可以 cargo run
并查看它的运行情况。
更新扩散限制聚集
这与之前的类似,因此我们将再次仅提供 dla.rs
的代码:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, TileType, Position, Symmetry, paint}; use rltk::RandomNumberGenerator; #[derive(PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum DLAAlgorithm { WalkInwards, WalkOutwards, CentralAttractor } pub struct DLABuilder { algorithm : DLAAlgorithm, brush_size: i32, symmetry: Symmetry, floor_percent: f32, } impl InitialMapBuilder for DLABuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl DLABuilder { #[allow(dead_code)] pub fn new() -> Box<DLABuilder> { Box::new(DLABuilder{ algorithm: DLAAlgorithm::WalkInwards, brush_size: 2, symmetry: Symmetry::None, floor_percent: 0.25, }) } #[allow(dead_code)] pub fn walk_inwards() -> Box<DLABuilder> { Box::new(DLABuilder{ algorithm: DLAAlgorithm::WalkInwards, brush_size: 1, symmetry: Symmetry::None, floor_percent: 0.25, }) } #[allow(dead_code)] pub fn walk_outwards() -> Box<DLABuilder> { Box::new(DLABuilder{ algorithm: DLAAlgorithm::WalkOutwards, brush_size: 2, symmetry: Symmetry::None, floor_percent: 0.25, }) } #[allow(dead_code)] pub fn central_attractor() -> Box<DLABuilder> { Box::new(DLABuilder{ algorithm: DLAAlgorithm::CentralAttractor, brush_size: 2, symmetry: Symmetry::None, floor_percent: 0.25, }) } #[allow(dead_code)] pub fn insectoid() -> Box<DLABuilder> { Box::new(DLABuilder{ algorithm: DLAAlgorithm::CentralAttractor, brush_size: 2, symmetry: Symmetry::Horizontal, floor_percent: 0.25, }) } #[allow(clippy::map_entry)] fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 雕刻起始种子 let starting_position = Position{ x: build_data.map.width/2, y : build_data.map.height/2 }; let start_idx = build_data.map.xy_idx(starting_position.x, starting_position.y); build_data.take_snapshot(); build_data.map.tiles[start_idx] = TileType::Floor; build_data.map.tiles[start_idx-1] = TileType::Floor; build_data.map.tiles[start_idx+1] = TileType::Floor; build_data.map.tiles[start_idx-build_data.map.width as usize] = TileType::Floor; build_data.map.tiles[start_idx+build_data.map.width as usize] = TileType::Floor; // 随机游走者 let total_tiles = build_data.map.width * build_data.map.height; let desired_floor_tiles = (self.floor_percent * total_tiles as f32) as usize; let mut floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); while floor_tile_count < desired_floor_tiles { match self.algorithm { DLAAlgorithm::WalkInwards => { let mut digger_x = rng.roll_dice(1, build_data.map.width - 3) + 1; let mut digger_y = rng.roll_dice(1, build_data.map.height - 3) + 1; let mut prev_x = digger_x; let mut prev_y = digger_y; let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); while build_data.map.tiles[digger_idx] == TileType::Wall { prev_x = digger_x; prev_y = digger_y; let stagger_direction = rng.roll_dice(1, 4); match stagger_direction { 1 => { if digger_x > 2 { digger_x -= 1; } } 2 => { if digger_x < build_data.map.width-2 { digger_x += 1; } } 3 => { if digger_y > 2 { digger_y -=1; } } _ => { if digger_y < build_data.map.height-2 { digger_y += 1; } } } digger_idx = build_data.map.xy_idx(digger_x, digger_y); } paint(&mut build_data.map, self.symmetry, self.brush_size, prev_x, prev_y); } DLAAlgorithm::WalkOutwards => { let mut digger_x = starting_position.x; let mut digger_y = starting_position.y; let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); while build_data.map.tiles[digger_idx] == TileType::Floor { let stagger_direction = rng.roll_dice(1, 4); match stagger_direction { 1 => { if digger_x > 2 { digger_x -= 1; } } 2 => { if digger_x < build_data.map.width-2 { digger_x += 1; } } 3 => { if digger_y > 2 { digger_y -=1; } } _ => { if digger_y < build_data.map.height-2 { digger_y += 1; } } } digger_idx = build_data.map.xy_idx(digger_x, digger_y); } paint(&mut build_data.map, self.symmetry, self.brush_size, digger_x, digger_y); } DLAAlgorithm::CentralAttractor => { let mut digger_x = rng.roll_dice(1, build_data.map.width - 3) + 1; let mut digger_y = rng.roll_dice(1, build_data.map.height - 3) + 1; let mut prev_x = digger_x; let mut prev_y = digger_y; let mut digger_idx = build_data.map.xy_idx(digger_x, digger_y); let mut path = rltk::line2d( rltk::LineAlg::Bresenham, rltk::Point::new( digger_x, digger_y ), rltk::Point::new( starting_position.x, starting_position.y ) ); while build_data.map.tiles[digger_idx] == TileType::Wall && !path.is_empty() { prev_x = digger_x; prev_y = digger_y; digger_x = path[0].x; digger_y = path[0].y; path.remove(0); digger_idx = build_data.map.xy_idx(digger_x, digger_y); } paint(&mut build_data.map, self.symmetry, self.brush_size, prev_x, prev_y); } } build_data.take_snapshot(); floor_tile_count = build_data.map.tiles.iter().filter(|a| **a == TileType::Floor).count(); } } } }
更新迷宫构建器
再次,这是 maze.rs
的代码:
#![allow(unused)] fn main() { use super::{Map, InitialMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; pub struct MazeBuilder {} impl InitialMapBuilder for MazeBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl MazeBuilder { #[allow(dead_code)] pub fn new() -> Box<MazeBuilder> { Box::new(MazeBuilder{}) } #[allow(clippy::map_entry)] fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 迷宫生成 let mut maze = Grid::new((build_data.map.width / 2)-2, (build_data.map.height / 2)-2, rng); maze.generate_maze(build_data); } } /* 迷宫代码根据 MIT 许可取自 https://github.com/cyucelen/mazeGenerator/ */ const TOP : usize = 0; const RIGHT : usize = 1; const BOTTOM : usize = 2; const LEFT : usize = 3; #[derive(Copy, Clone)] struct Cell { row: i32, column: i32, walls: [bool; 4], visited: bool, } impl Cell { fn new(row: i32, column: i32) -> Cell { Cell{ row, column, walls: [true, true, true, true], visited: false } } fn remove_walls(&mut self, next : &mut Cell) { let x = self.column - next.column; let y = self.row - next.row; if x == 1 { self.walls[LEFT] = false; next.walls[RIGHT] = false; } else if x == -1 { self.walls[RIGHT] = false; next.walls[LEFT] = false; } else if y == 1 { self.walls[TOP] = false; next.walls[BOTTOM] = false; } else if y == -1 { self.walls[BOTTOM] = false; next.walls[TOP] = false; } } } struct Grid<'a> { width: i32, height: i32, cells: Vec<Cell>, backtrace: Vec<usize>, current: usize, rng : &'a mut RandomNumberGenerator } impl<'a> Grid<'a> { fn new(width: i32, height:i32, rng: &mut RandomNumberGenerator) -> Grid { let mut grid = Grid{ width, height, cells: Vec::new(), backtrace: Vec::new(), current: 0, rng }; for row in 0..height { for column in 0..width { grid.cells.push(Cell::new(row, column)); } } grid } fn calculate_index(&self, row: i32, column: i32) -> i32 { if row < 0 || column < 0 || column > self.width-1 || row > self.height-1 { -1 } else { column + (row * self.width) } } fn get_available_neighbors(&self) -> Vec<usize> { let mut neighbors : Vec<usize> = Vec::new(); let current_row = self.cells[self.current].row; let current_column = self.cells[self.current].column; let neighbor_indices : [i32; 4] = [ self.calculate_index(current_row -1, current_column), self.calculate_index(current_row, current_column + 1), self.calculate_index(current_row + 1, current_column), self.calculate_index(current_row, current_column - 1) ]; for i in neighbor_indices.iter() { if *i != -1 && !self.cells[*i as usize].visited { neighbors.push(*i as usize); } } neighbors } fn find_next_cell(&mut self) -> Option<usize> { let neighbors = self.get_available_neighbors(); if !neighbors.is_empty() { if neighbors.len() == 1 { return Some(neighbors[0]); } else { return Some(neighbors[(self.rng.roll_dice(1, neighbors.len() as i32)-1) as usize]); } } None } fn generate_maze(&mut self, build_data : &mut BuilderMap) { let mut i = 0; loop { self.cells[self.current].visited = true; let next = self.find_next_cell(); match next { Some(next) => { self.cells[next].visited = true; self.backtrace.push(self.current); // __lower_part__ __higher_part_ // / \ / \ // --------cell1------ | cell2----------- let (lower_part, higher_part) = self.cells.split_at_mut(std::cmp::max(self.current, next)); let cell1 = &mut lower_part[std::cmp::min(self.current, next)]; let cell2 = &mut higher_part[0]; cell1.remove_walls(cell2); self.current = next; } None => { if !self.backtrace.is_empty() { self.current = self.backtrace[0]; self.backtrace.remove(0); } else { break; } } } if i % 50 == 0 { self.copy_to_map(&mut build_data.map); build_data.take_snapshot(); } i += 1; } } fn copy_to_map(&self, map : &mut Map) { // 清空地图 for i in map.tiles.iter_mut() { *i = TileType::Wall; } for cell in self.cells.iter() { let x = cell.column + 1; let y = cell.row + 1; let idx = map.xy_idx(x * 2, y * 2); map.tiles[idx] = TileType::Floor; if !cell.walls[TOP] { map.tiles[idx - map.width as usize] = TileType::Floor } if !cell.walls[RIGHT] { map.tiles[idx + 1] = TileType::Floor } if !cell.walls[BOTTOM] { map.tiles[idx + map.width as usize] = TileType::Floor } if !cell.walls[LEFT] { map.tiles[idx - 1] = TileType::Floor } } } } }
更新 Voronoi 地图
这是 Voronoi 构建器(在 voronoi.rs
中)的更新代码:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, BuilderMap, TileType}; use rltk::RandomNumberGenerator; #[derive(PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum DistanceAlgorithm { Pythagoras, Manhattan, Chebyshev } pub struct VoronoiCellBuilder { n_seeds: usize, distance_algorithm: DistanceAlgorithm } impl InitialMapBuilder for VoronoiCellBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl VoronoiCellBuilder { #[allow(dead_code)] pub fn new() -> Box<VoronoiCellBuilder> { Box::new(VoronoiCellBuilder{ n_seeds: 64, distance_algorithm: DistanceAlgorithm::Pythagoras, }) } #[allow(dead_code)] pub fn pythagoras() -> Box<VoronoiCellBuilder> { Box::new(VoronoiCellBuilder{ n_seeds: 64, distance_algorithm: DistanceAlgorithm::Pythagoras, }) } #[allow(dead_code)] pub fn manhattan() -> Box<VoronoiCellBuilder> { Box::new(VoronoiCellBuilder{ n_seeds: 64, distance_algorithm: DistanceAlgorithm::Manhattan, }) } #[allow(clippy::map_entry)] fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 制作 Voronoi 图。我们将以困难的方式做到这一点,以了解这项技术! let mut voronoi_seeds : Vec<(usize, rltk::Point)> = Vec::new(); while voronoi_seeds.len() < self.n_seeds { let vx = rng.roll_dice(1, build_data.map.width-1); let vy = rng.roll_dice(1, build_data.map.height-1); let vidx = build_data.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 ; build_data.map.width as usize * build_data.map.height as usize]; for (i, vid) in voronoi_membership.iter_mut().enumerate() { let x = i as i32 % build_data.map.width; let y = i as i32 / build_data.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..build_data.map.height-1 { for x in 1..build_data.map.width-1 { let mut neighbors = 0; let my_idx = build_data.map.xy_idx(x, y); let my_seed = voronoi_membership[my_idx]; if voronoi_membership[build_data.map.xy_idx(x-1, y)] != my_seed { neighbors += 1; } if voronoi_membership[build_data.map.xy_idx(x+1, y)] != my_seed { neighbors += 1; } if voronoi_membership[build_data.map.xy_idx(x, y-1)] != my_seed { neighbors += 1; } if voronoi_membership[build_data.map.xy_idx(x, y+1)] != my_seed { neighbors += 1; } if neighbors < 2 { build_data.map.tiles[my_idx] = TileType::Floor; } } build_data.take_snapshot(); } } } }
更新波函数坍缩
波函数坍缩是一个略有不同的端口,因为它已经有了“前一个构建器”的概念。现在这个概念已经消失了(链式调用是自动的),因此需要更新更多内容。波函数坍缩是一个元构建器,因此它实现了该 trait,而不是初始地图构建器。总的来说,这些更改使它 简单 得多!所有更改都在 waveform_collapse/mod.rs
中进行:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap, Map, TileType}; use rltk::RandomNumberGenerator; mod common; use common::*; mod constraints; use constraints::*; mod solver; use solver::*; /// 提供一个使用波函数坍缩算法的地图构建器。 pub struct WaveformCollapseBuilder {} impl MetaMapBuilder for WaveformCollapseBuilder { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl WaveformCollapseBuilder { /// 波函数坍缩的构造函数。 #[allow(dead_code)] pub fn new() -> Box<WaveformCollapseBuilder> { Box::new(WaveformCollapseBuilder{}) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { const CHUNK_SIZE :i32 = 8; build_data.take_snapshot(); let patterns = build_patterns(&build_data.map, CHUNK_SIZE, true, true); let constraints = patterns_to_constraints(patterns, CHUNK_SIZE); self.render_tile_gallery(&constraints, CHUNK_SIZE, build_data); build_data.map = Map::new(build_data.map.depth); loop { let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE, &build_data.map); while !solver.iteration(&mut build_data.map, rng) { build_data.take_snapshot(); } build_data.take_snapshot(); if solver.possible { break; } // 如果它遇到了不可能的条件,请重试 } build_data.spawn_list.clear(); } fn render_tile_gallery(&mut self, constraints: &[MapChunk], chunk_size: i32, build_data : &mut BuilderMap) { build_data.map = Map::new(0); let mut counter = 0; let mut x = 1; let mut y = 1; while counter < constraints.len() { render_pattern_to_map(&mut build_data.map, &constraints[counter], chunk_size, x, y); x += chunk_size + 1; if x + chunk_size > build_data.map.width { // 移动到下一行 x = 1; y += chunk_size + 1; if y + chunk_size > build_data.map.height { // 移动到下一页 build_data.take_snapshot(); build_data.map = Map::new(0); x = 1; y = 1; } } counter += 1; } build_data.take_snapshot(); } } }
您可以使用以下代码进行测试:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(VoronoiCellBuilder::pythagoras()); builder.with(WaveformCollapseBuilder::new()); builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); builder.with(CullUnreachable::new()); builder.with(VoronoiSpawning::new()); builder.with(DistantExit::new()); builder }
更新预制件构建器
这是一个有趣的东西。PrefabBuilder
既是 InitialMapBuilder
又是 MetaMapBuilder
- 两者之间共享代码。幸运的是,这些 traits 是相同的 - 因此我们可以同时实现它们,并从每个 trait 调用到主 build
函数中!Rust 足够智能,可以根据我们存储的 trait 找出我们正在调用哪个 trait - 因此 PrefabBuilder
可以放置在初始或元地图构建器中。
所有更改都在 prefab_builder/mod.rs
中进行:
#![allow(unused)] fn main() { use super::{InitialMapBuilder, MetaMapBuilder, BuilderMap, TileType, Position}; use rltk::RandomNumberGenerator; pub mod prefab_levels; pub mod prefab_sections; pub mod prefab_rooms; use std::collections::HashSet; #[derive(PartialEq, Copy, Clone)] #[allow(dead_code)] pub enum PrefabMode { RexLevel{ template : &'static str }, Constant{ level : prefab_levels::PrefabLevel }, Sectional{ section : prefab_sections::PrefabSection }, RoomVaults } #[allow(dead_code)] pub struct PrefabBuilder { mode: PrefabMode } impl MetaMapBuilder for PrefabBuilder { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl InitialMapBuilder for PrefabBuilder { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl PrefabBuilder { #[allow(dead_code)] pub fn new() -> Box<PrefabBuilder> { Box::new(PrefabBuilder{ mode : PrefabMode::RoomVaults, }) } #[allow(dead_code)] pub fn rex_level(template : &'static str) -> Box<PrefabBuilder> { Box::new(PrefabBuilder{ mode : PrefabMode::RexLevel{ template }, }) } #[allow(dead_code)] pub fn constant(level : prefab_levels::PrefabLevel) -> Box<PrefabBuilder> { Box::new(PrefabBuilder{ mode : PrefabMode::Constant{ level }, }) } #[allow(dead_code)] pub fn sectional(section : prefab_sections::PrefabSection) -> Box<PrefabBuilder> { Box::new(PrefabBuilder{ mode : PrefabMode::Sectional{ section }, }) } #[allow(dead_code)] pub fn vaults() -> Box<PrefabBuilder> { Box::new(PrefabBuilder{ mode : PrefabMode::RoomVaults, }) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { match self.mode { PrefabMode::RexLevel{template} => self.load_rex_map(&template, build_data), PrefabMode::Constant{level} => self.load_ascii_map(&level, build_data), PrefabMode::Sectional{section} => self.apply_sectional(§ion, rng, build_data), PrefabMode::RoomVaults => self.apply_room_vaults(rng, build_data) } build_data.take_snapshot(); } fn char_to_map(&mut self, ch : char, idx: usize, build_data : &mut BuilderMap) { match ch { ' ' => build_data.map.tiles[idx] = TileType::Floor, '#' => build_data.map.tiles[idx] = TileType::Wall, '@' => { let x = idx as i32 % build_data.map.width; let y = idx as i32 / build_data.map.width; build_data.map.tiles[idx] = TileType::Floor; build_data.starting_position = Some(Position{ x:x as i32, y:y as i32 }); } '>' => build_data.map.tiles[idx] = TileType::DownStairs, 'g' => { build_data.map.tiles[idx] = TileType::Floor; build_data.spawn_list.push((idx, "Goblin".to_string())); } 'o' => { build_data.map.tiles[idx] = TileType::Floor; build_data.spawn_list.push((idx, "Orc".to_string())); } '^' => { build_data.map.tiles[idx] = TileType::Floor; build_data.spawn_list.push((idx, "Bear Trap".to_string())); } '%' => { build_data.map.tiles[idx] = TileType::Floor; build_data.spawn_list.push((idx, "Rations".to_string())); } '!' => { build_data.map.tiles[idx] = TileType::Floor; build_data.spawn_list.push((idx, "Health Potion".to_string())); } _ => { rltk::console::log(format!("Unknown glyph loading map: {}", (ch as u8) as char)); } } } #[allow(dead_code)] fn load_rex_map(&mut self, path: &str, build_data : &mut BuilderMap) { let xp_file = rltk::rex::XpFile::from_resource(path).unwrap(); for layer in &xp_file.layers { for y in 0..layer.height { for x in 0..layer.width { let cell = layer.get(x, y).unwrap(); if x < build_data.map.width as usize && y < build_data.map.height as usize { let idx = build_data.map.xy_idx(x as i32, y as i32); // 我们正在做一些令人讨厌的类型转换,以便更容易在 match 中键入诸如 '#' 之类的东西 self.char_to_map(cell.ch as u8 as char, idx, build_data); } } } } } fn read_ascii_to_vec(template : &str) -> Vec<char> { let mut string_vec : Vec<char> = template.chars().filter(|a| *a != '\r' && *a !='\n').collect(); for c in string_vec.iter_mut() { if *c as u8 == 160u8 { *c = ' '; } } string_vec } #[allow(dead_code)] fn load_ascii_map(&mut self, level: &prefab_levels::PrefabLevel, build_data : &mut BuilderMap) { let string_vec = PrefabBuilder::read_ascii_to_vec(level.template); let mut i = 0; for ty in 0..level.height { for tx in 0..level.width { if tx < build_data.map.width as usize && ty < build_data.map.height as usize { let idx = build_data.map.xy_idx(tx as i32, ty as i32); if i < string_vec.len() { self.char_to_map(string_vec[i], idx, build_data); } } i += 1; } } } fn apply_previous_iteration<F>(&mut self, mut filter: F, _rng: &mut RandomNumberGenerator, build_data : &mut BuilderMap) where F : FnMut(i32, i32) -> bool { let width = build_data.map.width; build_data.spawn_list.retain(|(idx, _name)| { let x = *idx as i32 % width; let y = *idx as i32 / width; filter(x, y) }); build_data.take_snapshot(); } #[allow(dead_code)] fn apply_sectional(&mut self, section : &prefab_sections::PrefabSection, rng: &mut RandomNumberGenerator, build_data : &mut BuilderMap) { use prefab_sections::*; let string_vec = PrefabBuilder::read_ascii_to_vec(section.template); // 放置新 section let chunk_x; match section.placement.0 { HorizontalPlacement::Left => chunk_x = 0, HorizontalPlacement::Center => chunk_x = (build_data.map.width / 2) - (section.width as i32 / 2), HorizontalPlacement::Right => chunk_x = (build_data.map.width-1) - section.width as i32 } let chunk_y; match section.placement.1 { VerticalPlacement::Top => chunk_y = 0, VerticalPlacement::Center => chunk_y = (build_data.map.height / 2) - (section.height as i32 / 2), VerticalPlacement::Bottom => chunk_y = (build_data.map.height-1) - section.height as i32 } // 构建地图 self.apply_previous_iteration(|x,y| { x < chunk_x || x > (chunk_x + section.width as i32) || y < chunk_y || y > (chunk_y + section.height as i32) }, rng, build_data); let mut i = 0; for ty in 0..section.height { for tx in 0..section.width { if tx > 0 && tx < build_data.map.width as usize -1 && ty < build_data.map.height as usize -1 && ty > 0 { let idx = build_data.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); if i < string_vec.len() { self.char_to_map(string_vec[i], idx, build_data); } } i += 1; } } build_data.take_snapshot(); } fn apply_room_vaults(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { use prefab_rooms::*; // 应用之前的构建器,并保留它生成的所有实体(目前) self.apply_previous_iteration(|_x,_y| true, rng, build_data); // 我们想要 vault 吗? let vault_roll = rng.roll_dice(1, 6) + build_data.map.depth; if vault_roll < 4 { return; } // 请注意,这是一个占位符,将被移出此函数 let master_vault_list = vec![TOTALLY_NOT_A_TRAP, CHECKERBOARD, SILLY_SMILE]; // 将 vault 列表过滤到适用于当前深度的 vault let mut possible_vaults : Vec<&PrefabRoom> = master_vault_list .iter() .filter(|v| { build_data.map.depth >= v.first_depth && build_data.map.depth <= v.last_depth }) .collect(); if possible_vaults.is_empty() { return; } // 如果没有什么可构建的,则退出 let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len() as i32); let mut used_tiles : HashSet<usize> = HashSet::new(); for _i in 0..n_vaults { let vault_index = if possible_vaults.len() == 1 { 0 } else { (rng.roll_dice(1, possible_vaults.len() as i32)-1) as usize }; let vault = possible_vaults[vault_index]; // 我们将创建一个 vault 可能适合的位置列表 let mut vault_positions : Vec<Position> = Vec::new(); let mut idx = 0usize; loop { let x = (idx % build_data.map.width as usize) as i32; let y = (idx / build_data.map.width as usize) as i32; // 检查我们是否不会溢出地图 if x > 1 && (x+vault.width as i32) < build_data.map.width-2 && y > 1 && (y+vault.height as i32) < build_data.map.height-2 { let mut possible = true; for ty in 0..vault.height as i32 { for tx in 0..vault.width as i32 { let idx = build_data.map.xy_idx(tx + x, ty + y); if build_data.map.tiles[idx] != TileType::Floor { possible = false; } if used_tiles.contains(&idx) { possible = false; } } } if possible { vault_positions.push(Position{ x,y }); break; } } idx += 1; if idx >= build_data.map.tiles.len()-1 { break; } } if !vault_positions.is_empty() { let pos_idx = if vault_positions.len()==1 { 0 } else { (rng.roll_dice(1, vault_positions.len() as i32)-1) as usize }; let pos = &vault_positions[pos_idx]; let chunk_x = pos.x; let chunk_y = pos.y; let width = build_data.map.width; // 当我们在 `retain` 内部访问 `self` 时,借用检查器真的不喜欢这样 let height = build_data.map.height; // build_data.spawn_list.retain(|e| { let idx = e.0 as i32; let x = idx % width; let y = idx / height; x < chunk_x || x > chunk_x + vault.width as i32 || y < chunk_y || y > chunk_y + vault.height as i32 }); let string_vec = PrefabBuilder::read_ascii_to_vec(vault.template); let mut i = 0; for ty in 0..vault.height { for tx in 0..vault.width { let idx = build_data.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); if i < string_vec.len() { self.char_to_map(string_vec[i], idx, build_data); } used_tiles.insert(idx); i += 1; } } build_data.take_snapshot(); possible_vaults.remove(vault_index); } } } } }
您可以使用 random_builder
中的以下代码测试我们最近的更改(在 map_builders/mod.rs
中):
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(VoronoiCellBuilder::pythagoras()); builder.with(WaveformCollapseBuilder::new()); builder.with(PrefabBuilder::vaults()); builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); builder.with(CullUnreachable::new()); builder.with(VoronoiSpawning::new()); builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT)); builder.with(DistantExit::new()); builder }
这演示了我们方法的强大之处 - 我们正在从小构建块中组合大量功能。在这个例子中,我们正在:
- 从 使用
VoronoiBuilder
在Pythagoras
模式下生成的地图 开始。 - 使用
WaveformCollapseBuilder
运行 修改 地图,这将像拼图游戏一样重新排列地图。 - 通过
PrefabBuilder
(在 Vaults 模式下)修改 地图,放置 vaults。 - 使用
AreaStartingPositions
修改 地图,指示我们希望在地图中间附近开始。 - 修改 地图以剔除无法到达的区域。
- 修改 地图以使用 Voronoi 生成方法生成实体。
- 修改 地图以添加地下堡垒,再次使用
PrefabBuilder
。 - 修改 地图以在最远的位置添加出口楼梯。
删除 MapBuilder Trait 和 common 中的位
现在我们已经有了构建器机制,我们可以删除一些旧代码了。从 common.rs
中,我们可以删除 remove_unreachable_areas_returning_most_distant
和 generate_voronoi_spawn_regions
;我们已经用构建器步骤替换了它们。
我们还可以打开 map_builders/mod.rs
并删除 MapBuilder
trait 及其实现:我们现在已经完全替换了它。
随机化
像往常一样,我们希望回到地图生成是随机的状态。我们将把这个过程分解为两个步骤。我们将创建一个新函数 random_initial_builder
,该函数掷骰子并选择 起始 构建器。它还返回一个 bool
,指示我们是否选择了提供房间数据的算法。基本函数应该看起来很熟悉,但我们已经摆脱了所有 Box::new
调用 - 构造函数现在为我们创建 boxes:
#![allow(unused)] fn main() { fn random_initial_builder(rng: &mut rltk::RandomNumberGenerator) -> (Box<dyn InitialMapBuilder>, bool) { let builder = rng.roll_dice(1, 17); let result : (Box<dyn InitialMapBuilder>, bool); match builder { 1 => result = (BspDungeonBuilder::new(), true), 2 => result = (BspInteriorBuilder::new(), true), 3 => result = (CellularAutomataBuilder::new(), false), 4 => result = (DrunkardsWalkBuilder::open_area(), false), 5 => result = (DrunkardsWalkBuilder::open_halls(), false), 6 => result = (DrunkardsWalkBuilder::winding_passages(), false), 7 => result = (DrunkardsWalkBuilder::fat_passages(), false), 8 => result = (DrunkardsWalkBuilder::fearful_symmetry(), false), 9 => result = (MazeBuilder::new(), false), 10 => result = (DLABuilder::walk_inwards(), false), 11 => result = (DLABuilder::walk_outwards(), false), 12 => result = (DLABuilder::central_attractor(), false), 13 => result = (DLABuilder::insectoid(), false), 14 => result = (VoronoiCellBuilder::pythagoras(), false), 15 => result = (VoronoiCellBuilder::manhattan(), false), 16 => result = (PrefabBuilder::constant(prefab_builder::prefab_levels::WFC_POPULATED), false), _ => result = (SimpleMapBuilder::new(), true) } result } }
这是一个非常简单的函数 - 我们掷骰子,匹配结果表并返回我们选择的构建器和房间信息。现在我们将修改我们的 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 (random_starter, has_rooms) = random_initial_builder(rng); builder.start_with(random_starter); if has_rooms { builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); } else { builder.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)); builder.with(CullUnreachable::new()); builder.with(VoronoiSpawning::new()); builder.with(DistantExit::new()); } 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 } }
这应该看起来很熟悉。此函数:
- 使用我们刚刚创建的函数选择一个随机房间。
- 如果构建器提供房间数据,我们将链式调用
RoomBasedSpawner
、RoomBasedStairs
和RoomBasedStartingPositiosn
- 房间数据所需的三个重要步骤。 - 如果构建器 不 提供房间信息,我们将链式调用
AreaStartingPosition
、CullUnreachable
、VoronoiSpawning
和DistantExit
- 我们过去在每个构建器内部应用的默认值。 - 我们掷一个 3 面骰子;如果结果是 1 - 我们应用
WaveformCollapseBuilder
来重新排列地图。 - 我们掷一个 20 面骰子;如果结果是 1 - 我们应用我们的地下堡垒预制件。
- 我们将 vault 创建应用于最终地图,从而有机会出现预制房间。
总结
这是一个 巨大的 章节,但我们完成了很多工作:
- 我们现在有一个一致的构建器接口,用于将任意数量的元地图修饰符链接到我们的构建链。这应该使我们能够构建我们想要的地图。
- 每个构建器现在都 只做一项任务 - 因此如果您需要修复/调试它们,那么去哪里就更加明显了。
- 构建器不再负责制作其他构建器 - 因此我们已经剔除了一大堆代码,并将 bug 潜入的机会转移到只有一个(简单的)控制流中。
这为下一章奠定了基础,下一章将研究更多使用过滤器来修改地图的方法。
...
本章的源代码可以在这里找到
在您的浏览器中使用 Web 程序集运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。