更深邃的洞穴


关于本教程

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

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

Hands-On Rust


我们已经有了第一层石灰岩洞穴,看起来相当不错。我们从设计文档中了解到,洞穴会通向一个矮人要塞,但看起来我们可以再享受一下我们的洞穴渲染器。让我们构建一个更深邃的洞穴关卡,重点是一个兽人和地精营地,以及外围的野生怪物。

更多的作弊!

现在是添加更多作弊功能的好时机,以便更轻松地处理后面的关卡。

按需治疗

当你死亡的时候感觉很糟糕,特别是当你的本意只是想看看你的新关卡设计!所以我们将添加一个新的作弊选项:治疗。打开 gui.rs,并编辑 cheat_menu 和相关的 result 类型:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum CheatMenuResult { NoResponse, Cancel, TeleportToExit, Heal }

pub fn show_cheat_mode(_gs : &mut State, ctx : &mut Rltk) -> CheatMenuResult {
    let count = 2;
    let mut y = (25 - (count / 2)) as i32;
    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Cheating!");
    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");

    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('T'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "Teleport to next level"); // 传送至下一关

    y += 1;
    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('H'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "Heal all wounds"); // 治疗所有伤口


    match ctx.key {
        None => CheatMenuResult::NoResponse,
        Some(key) => {
            match key {
                VirtualKeyCode::T => CheatMenuResult::TeleportToExit,
                VirtualKeyCode::H => CheatMenuResult::Heal,
                VirtualKeyCode::Escape => CheatMenuResult::Cancel,
                _ => CheatMenuResult::NoResponse
            }
        }
    }
}
}

然后访问 main.rs,并在作弊处理器中添加对治疗的支持:

#![allow(unused)]
fn main() {
gui::CheatMenuResult::Heal => {
    let player = self.ecs.fetch::<Entity>();
    let mut pools = self.ecs.write_storage::<Pools>();
    let mut player_pools = pools.get_mut(*player).unwrap();
    player_pools.hit_points.current = player_pools.hit_points.max;
    newrunstate = RunState::AwaitingInput;
}
}

完成这些后,你只需按两次键就可以在需要时获得免费治疗!这应该使探索我们后面的关卡变得更容易:

Screenshot

显示全部和上帝模式

另一个方便的功能是显示地图,特别是当您只想验证地图构建时。完全关闭死亡也是确保地图都在您认为应该在的位置的好方法!所以首先,我们将添加另外两个菜单项及其处理程序:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum CheatMenuResult { NoResponse, Cancel, TeleportToExit, Heal, Reveal, GodMode }

pub fn show_cheat_mode(_gs : &mut State, ctx : &mut Rltk) -> CheatMenuResult {
    let count = 4;
    let mut y = (25 - (count / 2)) as i32;
    ctx.draw_box(15, y-2, 31, (count+3) as i32, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK));
    ctx.print_color(18, y-2, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Cheating!");
    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");

    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('T'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "Teleport to next level"); // 传送至下一关

    y += 1;
    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('H'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "Heal all wounds"); // 治疗所有伤口

    y += 1;
    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('R'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "Reveal the map"); // 显示地图

    y += 1;
    ctx.set(17, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437('('));
    ctx.set(18, y, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), rltk::to_cp437('G'));
    ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));
    ctx.print(21, y, "God Mode (No Death)"); // 上帝模式(无死亡)

    match ctx.key {
        None => CheatMenuResult::NoResponse,
        Some(key) => {
            match key {
                VirtualKeyCode::T => CheatMenuResult::TeleportToExit,
                VirtualKeyCode::H => CheatMenuResult::Heal,
                VirtualKeyCode::R => CheatMenuResult::Reveal,
                VirtualKeyCode::G => CheatMenuResult::GodMode,
                VirtualKeyCode::Escape => CheatMenuResult::Cancel,
                _ => CheatMenuResult::NoResponse
            }
        }
    }
}
}

现在我们需要在 main.rs 中处理这些:

#![allow(unused)]
fn main() {
gui::CheatMenuResult::Reveal => {
    let mut map = self.ecs.fetch_mut::<Map>();
    for v in map.revealed_tiles.iter_mut() {
        *v = true;
    }
    newrunstate = RunState::AwaitingInput;
}
gui::CheatMenuResult::GodMode => {
    let player = self.ecs.fetch::<Entity>();
    let mut pools = self.ecs.write_storage::<Pools>();
    let mut player_pools = pools.get_mut(*player).unwrap();
    player_pools.god_mode = true;
    newrunstate = RunState::AwaitingInput;
}
}

Reveal 非常简单:将地图上的每个瓦片都设置为已显示。上帝模式是在 Pools 组件中设置一个尚不存在的变量,所以打开 components.rs,我们将添加它:

#![allow(unused)]
fn main() {
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct Pools {
    pub hit_points : Pool,
    pub mana : Pool,
    pub xp : i32,
    pub level : i32,
    pub total_weight : f32,
    pub total_initiative_penalty : f32,
    pub gold : f32,
    pub god_mode : bool
}
}

我们需要在创建 Pools 对象的 spawner.rsraws/rawmaster.rs 函数中将 god_mode 设置为 false。最后,对 damage_system.rs 进行快速调整,以关闭神祇的伤害:

#![allow(unused)]
fn main() {
...
for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() {
    if !stats.god_mode {
        stats.hit_points.current -= damage.amount.iter().sum::<i32>();
    }
    ...
}

现在您可以随时显示地图,并关闭承受伤害的能力:

Screenshot

这使得处理后期内容 容易得多,而无需一遍又一遍地玩(不过,最好时不时地玩一下,并找出错误)。

深邃洞穴基本布局

深邃洞穴仍然应该看起来很自然,但也应该有一个中心区域,地精类生物可以在其中扎营。我们在上一章中研究过的扩散限制聚集 (Diffusion-Limited Aggregation) 算法,特别是“中心吸引子 (central attractor)”模式,为基本布局提供了我们想要的几乎所有东西:

Screenshot

map_builders/mod.rs 中,我们将首先为第 4 级创建一个新条目:

#![allow(unused)]
fn main() {
pub fn level_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    rltk::console::log(format!("Depth: {}", new_depth));
    match new_depth {
        1 => town_builder(new_depth, rng, width, height),
        2 => forest_builder(new_depth, rng, width, height),
        3 => limestone_cavern_builder(new_depth, rng, width, height),
        4 => limestone_deep_cavern_builder(new_depth, rng, width, height),
        _ => random_builder(new_depth, rng, width, height)
    }
}
}

map/themes.rs 中,我们将告诉这个关卡也使用石灰石主题:

#![allow(unused)]
fn main() {
pub fn tile_glyph(idx: usize, map : &Map) -> (rltk::FontCharType, RGB, RGB) {
let (glyph, mut fg, mut bg) = match map.depth {
    4 => get_limestone_cavern_glyph(idx, map),
    3 => get_limestone_cavern_glyph(idx, map),
    2 => get_forest_glyph(idx, map),
    _ => get_tile_glyph_default(idx, map)
};
}

然后在 map_builders/limestone_cavern.rs 中,我们可以添加新函数。这是一个好的开始:

#![allow(unused)]
fn main() {
pub fn limestone_deep_cavern_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Deep Limestone Caverns");
    chain.start_with(DLABuilder::central_attractor());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP));
    chain.with(VoronoiSpawning::new());
    chain.with(DistantExit::new());
    chain.with(CaveDecorator::new());
    chain
}
}

这实际上为我们提供了一个相当可玩性的关卡;我们可以停在这里而不感到羞愧(尽管我们显然需要添加更多怪物)。但是我们还没有完成!我们希望地图中心有一个兽人营地。这听起来像是预制件 (prefab) 的工作!打开 map_builders/prefab_builder/prefab_sections.rs,我们将添加一个新的 sectional:

#![allow(unused)]
fn main() {
#[allow(dead_code)]
pub const ORC_CAMP : PrefabSection = PrefabSection{
    template : ORC_CAMP_TXT,
    width: 12,
    height: 12,
    placement: ( HorizontalPlacement::Center, VerticalPlacement::Center )
};

#[allow(dead_code)]
const ORC_CAMP_TXT : &str = "
            
 ≈≈≈≈o≈≈≈≈≈ 
 ≈☼      ☼≈ 
 ≈ g      ≈ 
 ≈        ≈ 
 ≈    g   ≈ 
 o   O    o 
 ≈        ≈ 
 ≈ g      ≈ 
 ≈    g   ≈ 
 ≈☼      ☼≈ 
 ≈≈≈≈o≈≈≈≈≈ 
            
";
}

这里有一些新的字形,所以我们还需要打开 map_builders/prefab_builder/mod.rs,找到 char_to_map 函数并将它们添加进去。波浪线表示水(提供一个受保护的护城河),太阳符号表示篝火。大写字母 O 是兽人首领。所以我们将它们添加到 match 函数中:

#![allow(unused)]
fn main() {
'≈' => build_data.map.tiles[idx] = TileType::DeepWater,
'O' => {
    build_data.map.tiles[idx] = TileType::Floor;
    build_data.spawn_list.push((idx, "Orc Leader".to_string()));
}
'☼' => {
    build_data.map.tiles[idx] = TileType::Floor;
    build_data.spawn_list.push((idx, "Watch Fire".to_string()));
}
}

然后我们修改构建链(在 limestone_deep_cavern_builder 中)以包含此内容:

#![allow(unused)]
fn main() {
pub fn limestone_deep_cavern_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain {
    let mut chain = BuilderChain::new(new_depth, width, height, "Deep Limestone Caverns");
    chain.start_with(DLABuilder::central_attractor());
    chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP));
    chain.with(VoronoiSpawning::new());
    chain.with(DistantExit::new());
    chain.with(CaveDecorator::new());
    chain.with(PrefabBuilder::sectional(super::prefab_builder::prefab_sections::ORC_CAMP));
    chain
}
}

我们还需要添加缺少的实体。“Watch Fire”和“Orc Leader”是新的。所以我们打开 spawns.json 并将它们添加进去。Watch Fire 是一个 prop:

{
    "name" : "Watch Fire",
    "renderable": {
        "glyph" : "☼",
        "fg" : "#FFFF55",
        "bg" : "#000000",
        "order" : 2
    },
    "hidden" : false,
    "light" : {
        "range" : 6,
        "color" : "#FFFF55"
    },
    "entry_trigger" : {
        "effects" : {
            "damage" : "6"
        }
    }
}

light 条目是新的!我们以前没有让 prop 生成光照(但这很有道理;黑暗的篝火会很奇怪)。它也会在进入时造成伤害,这也很合理 - 走进火堆对你的健康不利。支持光照需要进行一些快速更改。打开 raws/prop_structs.rs,我们将为 props 添加 light 条目的选项:

#![allow(unused)]
fn main() {
#[derive(Deserialize, Debug)]
pub struct Prop {
    pub name : String,
    pub renderable : Option<Renderable>,
    pub hidden : Option<bool>,
    pub blocks_tile : Option<bool>,
    pub blocks_visibility : Option<bool>,
    pub door_open : Option<bool>,
    pub entry_trigger : Option<EntryTrigger>,
    pub light : Option<super::mob_structs::MobLight>,
}
}

我们重用了 mobs 中的 MobLight,因为它是一样的东西。现在打开 raws/raw_master.rs,我们将编辑 spawn_named_prop 以包含此选项:

#![allow(unused)]
fn main() {
if let Some(light) = &prop_template.light {
    eb = eb.with(LightSource{ range: light.range, color : rltk::RGB::from_hex(&light.color).expect("Bad color") });
    eb = eb.with(Viewshed{ range: light.range, dirty: true, visible_tiles: Vec::new() });
}
}

如果您还记得,我们的光照代码使用可见性图来确定它可以照亮的位置 - 因此 prop 需要一个 viewshed。没关系,我们的 ECS 会支持我们并处理它(并且在第一次绘制后,它将永远不会重新计算 - 因为 prop 不会移动)。

最后,我们的 Orc Leader 进入 spawns.json 的 "mobs" 部分:

{
    "name" : "Orc Leader",
    "renderable": {
        "glyph" : "O",
        "fg" : "#FF0000",
        "bg" : "#000000",
        "order" : 1
    },
    "blocks_tile" : true,
    "vision_range" : 8,
    "movement" : "static",
    "attributes" : {},
    "faction" : "Cave Goblins",
    "gold" : "3d8",
    "equipped" : [ "Battleaxe", "Tower Shield", "Leather Armor", "Leather Boots" ],
    "level" : 2
},

他应该是一个挑战,但如果你赢了,你可以从他那里获得不错的现金和好的武器/盔甲。

如果您现在 cargo run,您将看到要塞已就位(我在图形中使用的是上帝模式):

Screenshot

所以 prefab 在那里 - 但存在一个真正的问题:玩家完全被兽人和地精淹没了!虽然这可能是现实的,但它让玩家几乎没有机会在到达这个关卡后生存下来。即使是巧妙的玩法,在相对开放的地图中,这种类型的猛攻也可能很快被证明是致命的。所以现在,我们将调整 spawns.json 中的 spawn table:

"spawn_table" : [
    { "name" : "Goblin", "weight" : 10, "min_depth" : 3, "max_depth" : 4 },
    { "name" : "Orc", "weight" : 1, "min_depth" : 4, "max_depth" : 100 },
    { "name" : "Health Potion", "weight" : 7, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Fireball Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
    { "name" : "Confusion Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
    { "name" : "Magic Missile Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Dagger", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Shield", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Longsword", "weight" : 1, "min_depth" : 3, "max_depth" : 100 },
    { "name" : "Tower Shield", "weight" : 1, "min_depth" : 3, "max_depth" : 100 },
    { "name" : "Rations", "weight" : 10, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Magic Mapping Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Bear Trap", "weight" : 5, "min_depth" : 0, "max_depth" : 100 },
    { "name" : "Battleaxe", "weight" : 1, "min_depth" : 2, "max_depth" : 100 },
    { "name" : "Kobold", "weight" : 15, "min_depth" : 3, "max_depth" : 3 },
    { "name" : "Rat", "weight" : 15, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Mangy Wolf", "weight" : 13, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Deer", "weight" : 14, "min_depth" : 2, "max_depth" : 2 },
    { "name" : "Bandit", "weight" : 9, "min_depth" : 2, "max_depth" : 3 },
    { "name" : "Bat", "weight" : 15, "min_depth" : 3, "max_depth" : 3 },
    { "name" : "Large Spider", "weight" : 3, "min_depth" : 3, "max_depth" : 3 },
    { "name" : "Gelatinous Cube", "weight" : 3, "min_depth" : 3, "max_depth" : 3 }
],

我们已经从 Orcs 中删除了 add_map_depth_to_weight,所以它们不会 到处都是,并将其他生物限制为不出现在此关卡中。由于我们知道我们正在中间添加一个完整的要塞,这很有意义:你现在更有可能获得有用的掉落物,并且有更多的开放空间。

还有一个视觉问题。深蓝色的深水不错,但在灰度模式下基本上是不可见的 - 如果你的显示器亮度没有调高,也很难看到。让我们在其中添加一点绿色,使其更可见。在 map/themes.rs 中(get_limestone_cavern_glyph 函数):

#![allow(unused)]
fn main() {
TileType::DeepWater => { glyph = rltk::to_cp437('▓'); fg = RGB::from_f32(0.2, 0.2, 1.0); }
}

这样就好多了:

Screenshot

更多生成物

让我们花一点时间为关卡引入更好的盔甲和武器,并使其有可能生成。玩家开始面临真正的挑战,因此他们需要一些可能的改进!我们将首先将锁子甲添加到 spawns.json 中:

{
    "name" : "Chainmail Armor",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Torso",
        "armor_class" : 2.0
    },
    "weight_lbs" : 20.0,
    "base_value" : 50.0,
    "initiative_penalty" : 1.0,
    "vendor_category" : "armor"
},

{
    "name" : "Chain Coif",
    "renderable": {
        "glyph" : "[",
        "fg" : "#00FF00",
        "bg" : "#000000",
        "order" : 2
    },
    "wearable" : {
        "slot" : "Head",
        "armor_class" : 1.0
    },
    "weight_lbs" : 5.0,
    "base_value" : 20.0,
    "initiative_penalty" : 0.5,
    "vendor_category" : "armor"
},

通过包含 vendor_category,这些物品已可供出售 - 因此如果您的玩家获得足够的现金,他们可以购买它们(如果他们花时间回家!)。 让我们也让它们偶尔从第 4 级开始掉落。在 spawns.jsonspawn_table 中:

{ "name" : "Leather Armor", "weight" : 1, "min_depth" : 2, "max_depth" : 100 },
{ "name" : "Leather Boots", "weight" : 1, "min_depth" : 2, "max_depth" : 100 },
{ "name" : "Chainmail Armor", "weight" : 1, "min_depth" : 4, "max_depth" : 100 },
{ "name" : "Chain Coif", "weight" : 1, "min_depth" : 4, "max_depth" : 100 },

我们也允许皮革盔甲作为宝藏掉落出现。 这应该有助于降低难度!

总结

又完成了一个关卡(更多的改进是可能的;它们 总是 可能的),游戏正在成型!你现在可以开辟森林之路,砍杀石灰岩洞穴关卡,并在有兽人要塞的深邃洞穴中挥砍。这开始听起来像是一场冒险!

...

本章的源代码可以在 这里 找到

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

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