地牢密室


关于本教程

本教程是免费开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!

如果您喜欢本教程并希望我继续创作,请考虑支持我的 Patreon

动手 Rust 编程


上一章内容过长,因此分成了两部分。 在上一章中,我们学习了如何加载预制地图和地图区块,修改了生成系统,以便 元构建器 可以影响先前构建器的生成模式,并演示了将整个地图块集成到关卡中。 在本章中,我们将深入探讨 房间密室 - 可以将自身集成到您的关卡中的预制内容。 因此,您可以手工制作一些房间,并使其无缝地融入您现有的地图中。

设计房间:完全不是陷阱

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_depthlast_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(&section),
        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 代码! 让我们逐步分析一下:

  1. 您会注意到我们为函数添加了一个 模板 类型。 fn apply_previous_iteration<F>。 这指定了我们在编写函数时并不知道 F 到底是什么。
  2. 第二个参数 (mut filter: F) 也是 F 类型。 因此,我们告诉函数签名接受模板类型作为参数。
  3. 在左大括号之前,我们添加了一个 where 子句。 这种子句类型 可用于 限制 泛型类型接受的内容。 在本例中,我们说 F 必须FnMutFnMut 是一个 函数指针,它允许更改状态(可变的;如果是不可变的,则为 Fn)。 然后我们指定函数的参数及其返回类型。 在函数内部,我们现在可以将 filter 视为函数 - 即使我们实际上没有编写一个函数。 我们要求该函数接受两个 i32(整数)和一个 (usize, String)tuple。 后者应该看起来很熟悉 - 它是我们的生成列表格式。 前两个是生成的 xy 坐标 - 我们传递它是为了避免调用者每次都进行数学运算。
  4. 然后我们运行我们在上一章中编写的 prev_builder 代码 - 它构建地图并获取地图数据本身,以及来自先前算法的 spawn_list
  5. 然后我们遍历生成列表,并计算每个实体的 x/y 坐标和地图索引。 我们使用此信息调用 filter,如果它返回 true,我们将其添加到 我们自己的 spawn_list 中。
  6. 最后,我们拍摄地图的快照,以便您可以查看正在运行的步骤。

这听起来非常复杂,但它所做的大部分工作是允许我们替换 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 中为每个实体接收 xye。 在这种情况下,我们正在根据 chunk_xchunk_ysection.widthsection.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_depthlast_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; }
}
}

本节中有很多代码(用于确定密室可能适合的所有位置)。 让我们逐步分析一下:

  1. 我们创建了一个新的 Position 向量。 这将包含我们 可以 生成密室的所有可能位置。
  2. 我们将 idx 设置为 0 - 我们计划遍历整个地图。
  3. 我们启动一个 loop - Rust 的循环类型,除非您调用 break,否则不会退出。
    1. 我们计算 xy 以了解我们在地图上的位置。
    2. 我们进行溢出检查; x 需要大于 1,并且 x+1 需要小于地图宽度。 我们对 y 和地图高度进行相同的检查。 如果我们在边界内:
      1. 我们将 possible 设置为 true。
      2. 我们迭代地图上 (x .. x+vault width), (y .. y + vault height) 范围内的每个图块 - 如果任何图块不是地板,我们将 possible 设置为 false
      3. 如果 可以 在此处放置密室,我们将该位置添加到步骤 1 中的 vault_positions 向量中。
    3. 我们将 idx 递增 1。
    4. 如果我们用完了地图,我们将跳出循环。

换句话说,我们快速扫描 整个地图,查找我们可以放置密室的所有位置 - 并创建一个可能的放置列表。 然后我们:

#![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();
}
}

因此,如果 密室的任何有效位置,我们将:

  1. 选择 vault_positions 向量中的随机条目 - 这就是我们将放置密室的位置。
  2. 使用 read_ascii_to_vec 读取 ASCII 码,就像我们在预制件和区块中所做的那样。
  3. 迭代密室数据并使用 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,则该元素将被 保留(保留) - 否则将被删除。 因此,在这里我们捕获 widthheight(以避免借用 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
}
}

我们现在充分利用了我们图层系统的可组合性! 我们的随机构建器现在:

  1. 在第一层中,我们滚动 1d17 并选择地图类型; 我们已将预制关卡作为选项之一包含在内。
  2. 接下来,我们滚动 1d3 - 并在 1 上,我们在 构建器上运行 WaveformCollapse 算法。
  3. 我们滚动 1d20,并在 1 上 - 我们应用 PrefabBuilder 区块,并添加我们的堡垒。 这样,您只会偶尔遇到它。
  4. 我们针对 PrefabBuilder 的房间密室系统(本章的重点!)运行我们提出的任何构建器,以将预制房间添加到混合中。

总结

在本章中,我们获得了预制房间的能力,并在它们适合我们的关卡设计时包含它们。 我们还探索了将算法组合在一起的能力,从而提供了更多层次的随机性。

...

本章的源代码可以在此处找到

在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)

版权所有 (C) 2019, Herbert Wolverson。