地牢密室
关于本教程
本教程是免费开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢本教程并希望我继续创作,请考虑支持我的 Patreon。
上一章内容过长,因此分成了两部分。 在上一章中,我们学习了如何加载预制地图和地图区块,修改了生成系统,以便 元构建器 可以影响先前构建器的生成模式,并演示了将整个地图块集成到关卡中。 在本章中,我们将深入探讨 房间密室 - 可以将自身集成到您的关卡中的预制内容。 因此,您可以手工制作一些房间,并使其无缝地融入您现有的地图中。
设计房间:完全不是陷阱
Roguelike 游戏开发者的生活一部分是程序员,一部分是室内设计师(以一种古怪的侏儒疯狂科学家的风格)。 我们已经设计了整个关卡和关卡区块,所以设计房间并不是一个巨大的飞跃。 让我们继续构建一些预先设计的房间。
我们将在 map_builders/prefab_builders
中创建一个名为 prefab_rooms.rs
的新文件。 我们将在其中插入一个相对标志性的地图特征:
#![allow(unused)] fn main() { #[allow(dead_code)] #[derive(PartialEq, Copy, Clone)] pub struct PrefabRoom { pub template : &'static str, pub width : usize, pub height: usize, pub first_depth: i32, pub last_depth: i32 } #[allow(dead_code)] pub const TOTALLY_NOT_A_TRAP : PrefabRoom = PrefabRoom{ template : TOTALLY_NOT_A_TRAP_MAP, width: 5, height: 5, first_depth: 0, last_depth: 100 }; #[allow(dead_code)] const TOTALLY_NOT_A_TRAP_MAP : &str = " ^^^ ^!^ ^^^ "; }
如果您查看 ASCII 码,您会看到一个经典的地图设计:一个生命药水完全被陷阱包围。 由于陷阱默认是隐藏的,我们指望玩家会想“嗯,这看起来一点也不可疑”! 注意到内容周围都有空格 - 周围有一圈 1 格的 沟槽。 这确保了放置密室的任何 5x5 房间仍然是可通行的。 我们还引入了 first_depth
和 last_depth
- 这些是密室 可能 应用的关卡; 为了便于介绍,我们选择 0..100 - 这应该是每个关卡,除非你是 真的 非常敬业的测试玩家!
放置“完全不是陷阱”房间
我们将首先在 PrefabBuiler
系统中添加另一种 模式:
#![allow(unused)] fn main() { #[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(unused)] fn main() { impl PrefabBuilder { #[allow(dead_code)] pub fn new(new_depth : i32, previous_builder : Option<Box<dyn MapBuilder>>) -> PrefabBuilder { PrefabBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history : Vec::new(), mode : PrefabMode::RoomVaults, previous_builder, spawn_list : Vec::new() } } ... }
我们将教导 build
中的 match
函数使用它:
#![allow(unused)] fn main() { fn build(&mut self) { match self.mode { PrefabMode::RexLevel{template} => self.load_rex_map(&template), PrefabMode::Constant{level} => self.load_ascii_map(&level), PrefabMode::Sectional{section} => self.apply_sectional(§ion), PrefabMode::RoomVaults => self.apply_room_vaults() } self.take_snapshot(); ... }
接下来合乎逻辑的步骤是编写 apply_room_vaults
。 我们的目标是扫描传入的地图(来自不同的构建器,甚至是这个构建器的先前迭代!),找到可以放置密室的合适位置,并将其添加到地图中。 我们还希望删除密室区域中任何已生成的生物 - 以便密室保持手工制作,并且不受随机生成的影响。
我们将重用我们在 apply_sectional
中的“创建先前迭代”代码 - 让我们将其重写为更通用的形式:
#![allow(unused)] fn main() { fn apply_previous_iteration<F>(&mut self, mut filter: F) where F : FnMut(i32, i32, &(usize, String)) -> bool { // 构建地图 let prev_builder = self.previous_builder.as_mut().unwrap(); prev_builder.build_map(); self.starting_position = prev_builder.get_starting_position(); self.map = prev_builder.get_map().clone(); for e in prev_builder.get_spawn_list().iter() { let idx = e.0; let x = idx as i32 % self.map.width; let y = idx as i32 / self.map.width; if filter(x, y, e) { self.spawn_list.push( (idx, e.1.to_string()) ) } } self.take_snapshot(); } }
这里有很多新的 Rust 代码! 让我们逐步分析一下:
- 您会注意到我们为函数添加了一个 模板 类型。
fn apply_previous_iteration<F>
。 这指定了我们在编写函数时并不知道F
到底是什么。 - 第二个参数 (
mut filter: F
) 也是F
类型。 因此,我们告诉函数签名接受模板类型作为参数。 - 在左大括号之前,我们添加了一个
where
子句。 这种子句类型 可用于 限制 泛型类型接受的内容。 在本例中,我们说F
必须 是FnMut
。FnMut
是一个 函数指针,它允许更改状态(可变的;如果是不可变的,则为Fn
)。 然后我们指定函数的参数及其返回类型。 在函数内部,我们现在可以将filter
视为函数 - 即使我们实际上没有编写一个函数。 我们要求该函数接受两个i32
(整数)和一个(usize, String)
的tuple
。 后者应该看起来很熟悉 - 它是我们的生成列表格式。 前两个是生成的x
和y
坐标 - 我们传递它是为了避免调用者每次都进行数学运算。 - 然后我们运行我们在上一章中编写的
prev_builder
代码 - 它构建地图并获取地图数据本身,以及来自先前算法的spawn_list
。 - 然后我们遍历生成列表,并计算每个实体的 x/y 坐标和地图索引。 我们使用此信息调用
filter
,如果它返回true
,我们将其添加到 我们自己的spawn_list
中。 - 最后,我们拍摄地图的快照,以便您可以查看正在运行的步骤。
这听起来非常复杂,但它所做的大部分工作是允许我们替换 apply_sectional
中的以下代码:
#![allow(unused)] fn main() { // 构建地图 let prev_builder = self.previous_builder.as_mut().unwrap(); prev_builder.build_map(); self.starting_position = prev_builder.get_starting_position(); self.map = prev_builder.get_map().clone(); for e in prev_builder.get_spawn_list().iter() { let idx = e.0; let x = idx as i32 % self.map.width; let y = idx as i32 / self.map.width; if x < chunk_x || x > (chunk_x + section.width as i32) || y < chunk_y || y > (chunk_y + section.height as i32) { self.spawn_list.push( (idx, e.1.to_string()) ) } } self.take_snapshot(); }
我们可以使用更通用的调用来替换它:
#![allow(unused)] fn main() { // 构建地图 self.apply_previous_iteration(|x,y,e| { x < chunk_x || x > (chunk_x + section.width as i32) || y < chunk_y || y > (chunk_y + section.height as i32) }); }
这很有趣:我们正在将一个 闭包 - 一个 lambda 函数传递给 filter
。 它从先前地图的 spawn_list
中为每个实体接收 x
、y
和 e
。 在这种情况下,我们正在根据 chunk_x
、chunk_y
、section.width
和 section.height
进行检查,以查看实体是否在我们区块内部。 您可能已经注意到我们没有在 lambda 函数中的任何位置声明这些; 我们依赖于 捕获 - 您可以调用 lambda 并引用 在其作用域内 的其他变量 - 并且它可以像引用自己的变量一样引用它们。 这是一个 非常 强大的功能,您可以在此处了解它。
房间密室
让我们开始构建 apply_room_vaults
。 我们将逐步进行,逐步完成。 我们将从函数签名开始:
#![allow(unused)] fn main() { fn apply_room_vaults(&mut self) { use prefab_rooms::*; let mut rng = RandomNumberGenerator::new(); }
足够简单:除了构建器的可变成员资格之外,没有其他参数。 它将引用 prefab_rooms
中的类型,因此与其每次都键入它,不如使用函数内 using statement
将名称导入本地命名空间以节省您的手指。 我们还需要一个随机数生成器,所以我们像以前一样创建一个。 接下来:
#![allow(unused)] fn main() { // 应用先前的构建器,并保留它生成的所有实体(目前) self.apply_previous_iteration(|_x,_y,_e| true); }
我们使用刚刚编写的代码来应用先前的地图。 这次我们传入的 filter
始终返回 true:暂时保留所有实体。 接下来:
#![allow(unused)] fn main() { // 请注意,这是一个占位符,稍后将移出此函数 let master_vault_list = vec![TOTALLY_NOT_A_TRAP]; // 将密室列表过滤为适用于当前深度的密室 let possible_vaults : Vec<&PrefabRoom> = master_vault_list .iter() .filter(|v| { self.depth >= v.first_depth && self.depth <= v.last_depth }) .collect(); if possible_vaults.is_empty() { return; } // 如果没有要构建的内容,则退出 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]; }
我们创建了一个包含所有可能的密室类型的向量 - 目前只有一个,但是当我们有更多时,它们会放在这里。 这并不是很理想,但我们将在以后的章节中考虑将其设为全局资源。 然后,我们通过获取 master_vault_list
并 过滤 它以仅包含那些 first_depth
和 last_depth
与请求的地牢深度一致的密室来创建 possible_vaults
列表。 iter().filter(...).collect()
模式之前已经描述过,它是一种非常强大的方式,可以快速提取您需要的向量内容。 如果没有可能的密室,我们 return
退出函数 - 这里没什么可做的! 最后,我们使用我们之前使用过的另一种模式:我们通过选择 possible_vaults
向量的随机成员来选择要创建的密室。
接下来:
#![allow(unused)] fn main() { // 我们将创建一个列表,列出密室可能适合的所有位置 let mut vault_positions : Vec<Position> = Vec::new(); let mut idx = 0usize; loop { let x = (idx % self.map.width as usize) as i32; let y = (idx / self.map.width as usize) as i32; // 检查我们是否会溢出地图 if x > 1 && (x+vault.width as i32) < self.map.width-2 && y > 1 && (y+vault.height as i32) < self.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 = self.map.xy_idx(tx + x, ty + y); if self.map.tiles[idx] != TileType::Floor { possible = false; } } } if possible { vault_positions.push(Position{ x,y }); break; } } idx += 1; if idx >= self.map.tiles.len()-1 { break; } } }
本节中有很多代码(用于确定密室可能适合的所有位置)。 让我们逐步分析一下:
- 我们创建了一个新的
Position
向量。 这将包含我们 可以 生成密室的所有可能位置。 - 我们将
idx
设置为0
- 我们计划遍历整个地图。 - 我们启动一个
loop
- Rust 的循环类型,除非您调用break
,否则不会退出。- 我们计算
x
和y
以了解我们在地图上的位置。 - 我们进行溢出检查;
x
需要大于 1,并且x+1
需要小于地图宽度。 我们对y
和地图高度进行相同的检查。 如果我们在边界内:- 我们将
possible
设置为 true。 - 我们迭代地图上
(x .. x+vault width), (y .. y + vault height)
范围内的每个图块 - 如果任何图块不是地板,我们将possible
设置为false
。 - 如果 可以 在此处放置密室,我们将该位置添加到步骤 1 中的
vault_positions
向量中。
- 我们将
- 我们将
idx
递增 1。 - 如果我们用完了地图,我们将跳出循环。
- 我们计算
换句话说,我们快速扫描 整个地图,查找我们可以放置密室的所有位置 - 并创建一个可能的放置列表。 然后我们:
#![allow(unused)] fn main() { 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 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 = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); self.char_to_map(string_vec[i], idx); i += 1; } } self.take_snapshot(); } }
因此,如果 有 密室的任何有效位置,我们将:
- 选择
vault_positions
向量中的随机条目 - 这就是我们将放置密室的位置。 - 使用
read_ascii_to_vec
读取 ASCII 码,就像我们在预制件和区块中所做的那样。 - 迭代密室数据并使用
char_to_map
放置它 - 就像我们之前所做的那样。
将它们放在一起,您将得到以下函数:
#![allow(unused)] fn main() { fn apply_room_vaults(&mut self) { use prefab_rooms::*; let mut rng = RandomNumberGenerator::new(); // 应用先前的构建器,并保留它生成的所有实体(目前) self.apply_previous_iteration(|_x,_y,_e| true); // 请注意,这是一个占位符,稍后将移出此函数 let master_vault_list = vec![TOTALLY_NOT_A_TRAP]; // 将密室列表过滤为适用于当前深度的密室 let possible_vaults : Vec<&PrefabRoom> = master_vault_list .iter() .filter(|v| { self.depth >= v.first_depth && self.depth <= v.last_depth }) .collect(); if possible_vaults.is_empty() { return; } // 如果没有要构建的内容,则退出 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]; // 我们将创建一个列表,列出密室可能适合的所有位置 let mut vault_positions : Vec<Position> = Vec::new(); let mut idx = 0usize; loop { let x = (idx % self.map.width as usize) as i32; let y = (idx / self.map.width as usize) as i32; // 检查我们是否会溢出地图 if x > 1 && (x+vault.width as i32) < self.map.width-2 && y > 1 && (y+vault.height as i32) < self.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 = self.map.xy_idx(tx + x, ty + y); if self.map.tiles[idx] != TileType::Floor { possible = false; } } } if possible { vault_positions.push(Position{ x,y }); break; } } idx += 1; if idx >= self.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 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 = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); self.char_to_map(string_vec[i], idx); i += 1; } } self.take_snapshot(); } } }
方形密室更可能适合矩形房间,因此我们将跳到 map_builders/mod.rs
并稍微调整 random_builder
以对基础地图使用原始的简单地图算法:
#![allow(unused)] fn main() { Box::new( PrefabBuilder::new( new_depth, Some( Box::new( SimpleMapBuilder::new(new_depth) ) ) ) ) }
如果您现在 cargo run
,密室可能会放置在您的地图上。 这是我运行并找到它的截图:
。
过滤实体
我们可能不希望保留先前地图迭代中位于我们新密室内的实体。 您可能会巧妙地放置一个陷阱,然后在上面生成一个地精!(虽然很有趣,但这可能不是您所想的)。 因此,我们将扩展 apply_room_vaults
以在放置密室时进行一些过滤。 我们希望在生成新内容 之前 进行过滤,然后再使用房间生成更多内容。 输入 retain
功能:
#![allow(unused)] fn main() { ... let chunk_y = pos.y; let width = self.map.width; // borrow checker 真的不喜欢 let height = self.map.height; // 当我们在 `retain` 内部访问 `self` 时 self.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 }); ... }
在向量上调用 retain
会遍历每个条目,并调用传递的闭包/lambda 函数。 如果它返回 true
,则该元素将被 保留(保留) - 否则将被删除。 因此,在这里我们捕获 width
和 height
(以避免借用 self
),然后计算每个条目的位置。 如果它在新密室之外 - 我们保留它。
我想要不止一个密室!
只有一个密室非常乏味 - 虽然在证明功能有效性方面是一个良好的开端。 在 prefab_rooms.rs
中,我们将继续编写更多密室。 这些并非旨在成为关卡设计的开创性示例,但它们说明了该过程。 我们将添加更多房间预制件:
#![allow(unused)] fn main() { #[allow(dead_code)] #[derive(PartialEq, Copy, Clone)] pub struct PrefabRoom { pub template : &'static str, pub width : usize, pub height: usize, pub first_depth: i32, pub last_depth: i32 } #[allow(dead_code)] pub const TOTALLY_NOT_A_TRAP : PrefabRoom = PrefabRoom{ template : TOTALLY_NOT_A_TRAP_MAP, width: 5, height: 5, first_depth: 0, last_depth: 100 }; #[allow(dead_code)] const TOTALLY_NOT_A_TRAP_MAP : &str = " ^^^ ^!^ ^^^ "; #[allow(dead_code)] pub const SILLY_SMILE : PrefabRoom = PrefabRoom{ template : SILLY_SMILE_MAP, width: 6, height: 6, first_depth: 0, last_depth: 100 }; #[allow(dead_code)] const SILLY_SMILE_MAP : &str = " ^ ^ # ### "; #[allow(dead_code)] pub const CHECKERBOARD : PrefabRoom = PrefabRoom{ template : CHECKERBOARD_MAP, width: 6, height: 6, first_depth: 0, last_depth: 100 }; #[allow(dead_code)] const CHECKERBOARD_MAP : &str = " ^# g#%# #!# ^# # "; }
我们添加了 CHECKERBOARD
(一个墙壁和空间网格,其中包含陷阱、一个地精和战利品),以及 SILLY_SMILE
,它看起来只是一个愚蠢的墙壁特征。 现在打开 map_builders/prefab_builder/mod.rs
中的 apply_room_vaults
并将它们添加到主向量中:
#![allow(unused)] fn main() { // 请注意,这是一个占位符,稍后将移出此函数 let master_vault_list = vec![TOTALLY_NOT_A_TRAP, CHECKERBOARD, SILLY_SMILE]; }
如果您现在 cargo run
,您很可能会遇到这三个密室之一。 每次您深入一层,您都可能会遇到这三个密室之一。 我的测试几乎立即遇到了棋盘格:
。
这是一个好的开始,并且在您下降时为地图增添了一些风格 - 但当您说您想要不止一个密室时,这可能与您所要求的并不完全一致! 一个关卡上不止一个密室 怎么样? 回到 apply_room_vaults
! 很容易想出要生成的密室数量:
#![allow(unused)] fn main() { let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len() as i32); }
这将 n_vaults
设置为骰子滚动 (1d3
) 和可能的密室数量的 最小值 - 因此它永远不会超过选项的数量,但可以稍微变化。 将创建函数包装在 for
循环中也很容易:
#![allow(unused)] fn main() { if possible_vaults.is_empty() { return; } // 如果没有要构建的内容,则退出 let n_vaults = i32::min(rng.roll_dice(1, 3), possible_vaults.len() as i32); 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]; ... self.take_snapshot(); possible_vaults.remove(vault_index); } } }
请注意,在循环的 末尾,我们正在从 possible_vaults
中删除我们添加的密室。 我们必须更改声明才能做到这一点:let mut possible_vaults : Vec<&PrefabRoom> = ...
- 我们添加 mut
以允许我们更改向量。 这样,我们就不会一直添加 相同 的密室 - 它们只生成一次。
现在是更困难的部分:确保我们的新密室不与先前生成的密室重叠。 我们将创建一个新的 HashSet
,其中包含我们已消耗的图块:
#![allow(unused)] fn main() { let mut used_tiles : HashSet<usize> = HashSet::new(); }
哈希集合的优点是提供了一种快速判断它们是否包含值的方法,因此它们非常适合我们的需求。 当我们添加图块时,我们将图块 idx
插入到集合中:
#![allow(unused)] fn main() { for ty in 0..vault.height { for tx in 0..vault.width { let idx = self.map.xy_idx(tx as i32 + chunk_x, ty as i32 + chunk_y); self.char_to_map(string_vec[i], idx); used_tiles.insert(idx); i += 1; } } }
最后,在我们的可能性检查中,我们想要针对 used_tiles
进行检查,以确保我们没有重叠:
#![allow(unused)] fn main() { let idx = self.map.xy_idx(tx + x, ty + y); if self.map.tiles[idx] != TileType::Floor { possible = false; } if used_tiles.contains(&idx) { possible = false; } }
现在,如果您 cargo run
您的项目,您可能会遇到多个密室。 这是一个我们遇到两个密室的案例:
。
我不 总是 想要密室!
如果您在每个关卡都提供所有密室,那么游戏将比您可能想要的更可预测(除非您制作 很多 密室!)。 我们将修改 apply_room_vaults
,使其仅在有时有密室,并且随着您深入地牢,概率会增加:
#![allow(unused)] fn main() { // 应用先前的构建器,并保留它生成的所有实体(目前) self.apply_previous_iteration(|_x,_y,_e| true); // 我们想要密室吗? let vault_roll = rng.roll_dice(1, 6) + self.depth; if vault_roll < 4 { return; } }
这非常简单:我们掷一个六面骰子并加上当前深度。 如果我们掷出的点数小于 4
,我们将退出并仅提供先前生成的地图。 如果您现在 cargo run
您的项目,您有时会遇到密室 - 有时您不会。
完成:提供除 new 之外的其他构造函数
我们应该提供一些更友好的方式来构建我们的 PrefabBuilder
,以便在我们构建构建器链时清楚地了解我们在做什么。 将以下构造函数添加到 prefab_builder/mod.rs
:
#![allow(unused)] fn main() { #[allow(dead_code)] pub fn rex_level(new_depth : i32, template : &'static str) -> PrefabBuilder { PrefabBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history : Vec::new(), mode : PrefabMode::RexLevel{ template }, previous_builder : None, spawn_list : Vec::new() } } #[allow(dead_code)] pub fn constant(new_depth : i32, level : prefab_levels::PrefabLevel) -> PrefabBuilder { PrefabBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history : Vec::new(), mode : PrefabMode::Constant{ level }, previous_builder : None, spawn_list : Vec::new() } } #[allow(dead_code)] pub fn sectional(new_depth : i32, section : prefab_sections::PrefabSection, previous_builder : Box<dyn MapBuilder>) -> PrefabBuilder { PrefabBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history : Vec::new(), mode : PrefabMode::Sectional{ section }, previous_builder : Some(previous_builder), spawn_list : Vec::new() } } #[allow(dead_code)] pub fn vaults(new_depth : i32, previous_builder : Box<dyn MapBuilder>) -> PrefabBuilder { PrefabBuilder{ map : Map::new(new_depth), starting_position : Position{ x: 0, y : 0 }, depth : new_depth, history : Vec::new(), mode : PrefabMode::RoomVaults, previous_builder : Some(previous_builder), spawn_list : Vec::new() } } }
我们现在有了一个用于创建元构建器的不错的接口!
无处不在的海龟(或元构建器)
最近的几章都创建了 元构建器 - 它们实际上不是 构建器,因为它们不会创建全新的地图,而是修改另一种算法的结果。 这里真正有趣的是,您可以将它们链接在一起以达到您想要的结果。 例如,让我们通过从细胞自动机地图开始,通过波函数坍缩传递它,可能添加城堡墙,然后搜索密室来制作地图!
此语法的当前形式非常丑陋(这将是未来章节的主题)。 在 map_builders/mod.rs
中:
#![allow(unused)] fn main() { Box::new( PrefabBuilder::vaults( new_depth, Box::new(PrefabBuilder::sectional( new_depth, prefab_builder::prefab_sections::UNDERGROUND_FORT, Box::new(WaveformCollapseBuilder::derived_map( new_depth, Box::new(CellularAutomataBuilder::new(new_depth)) )) )) ) ) }
同样在 map_builders/prefab_builder/mod.rs
中,确保您公开共享地图模块:
#![allow(unused)] fn main() { pub mod prefab_levels; pub mod prefab_sections; pub mod prefab_rooms; }
如果您 cargo run
这个,您将观看它循环遍历分层构建:
。
恢复随机性
既然我们已经完成了关于预制、分层地图构建的为期两章的马拉松,现在是时候恢复 random_builder
函数以再次提供随机性了。 这是来自 map_builders/mod.rs
的新函数:
#![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, 17); let mut result : Box<dyn MapBuilder>; match builder { 1 => { result = Box::new(BspDungeonBuilder::new(new_depth)); } 2 => { result = Box::new(BspInteriorBuilder::new(new_depth)); } 3 => { result = Box::new(CellularAutomataBuilder::new(new_depth)); } 4 => { result = Box::new(DrunkardsWalkBuilder::open_area(new_depth)); } 5 => { result = Box::new(DrunkardsWalkBuilder::open_halls(new_depth)); } 6 => { result = Box::new(DrunkardsWalkBuilder::winding_passages(new_depth)); } 7 => { result = Box::new(DrunkardsWalkBuilder::fat_passages(new_depth)); } 8 => { result = Box::new(DrunkardsWalkBuilder::fearful_symmetry(new_depth)); } 9 => { result = Box::new(MazeBuilder::new(new_depth)); } 10 => { result = Box::new(DLABuilder::walk_inwards(new_depth)); } 11 => { result = Box::new(DLABuilder::walk_outwards(new_depth)); } 12 => { result = Box::new(DLABuilder::central_attractor(new_depth)); } 13 => { result = Box::new(DLABuilder::insectoid(new_depth)); } 14 => { result = Box::new(VoronoiCellBuilder::pythagoras(new_depth)); } 15 => { result = Box::new(VoronoiCellBuilder::manhattan(new_depth)); } 16 => { result = Box::new(PrefabBuilder::constant(new_depth, prefab_builder::prefab_levels::WFC_POPULATED)) }, _ => { result = Box::new(SimpleMapBuilder::new(new_depth)); } } if rng.roll_dice(1, 3)==1 { result = Box::new(WaveformCollapseBuilder::derived_map(new_depth, result)); } if rng.roll_dice(1, 20)==1 { result = Box::new(PrefabBuilder::sectional(new_depth, prefab_builder::prefab_sections::UNDERGROUND_FORT ,result)); } result = Box::new(PrefabBuilder::vaults(new_depth, result)); result } }
我们现在充分利用了我们图层系统的可组合性! 我们的随机构建器现在:
- 在第一层中,我们滚动
1d17
并选择地图类型; 我们已将预制关卡作为选项之一包含在内。 - 接下来,我们滚动
1d3
- 并在 1 上,我们在 该 构建器上运行WaveformCollapse
算法。 - 我们滚动
1d20
,并在 1 上 - 我们应用PrefabBuilder
区块,并添加我们的堡垒。 这样,您只会偶尔遇到它。 - 我们针对
PrefabBuilder
的房间密室系统(本章的重点!)运行我们提出的任何构建器,以将预制房间添加到混合中。
总结
在本章中,我们获得了预制房间的能力,并在它们适合我们的关卡设计时包含它们。 我们还探索了将算法组合在一起的能力,从而提供了更多层次的随机性。
...
本章的源代码可以在此处找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。