更深邃的洞穴
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持 我的 Patreon。
我们已经有了第一层石灰岩洞穴,看起来相当不错。我们从设计文档中了解到,洞穴会通向一个矮人要塞,但看起来我们可以再享受一下我们的洞穴渲染器。让我们构建一个更深邃的洞穴关卡,重点是一个兽人和地精营地,以及外围的野生怪物。
更多的作弊!
现在是添加更多作弊功能的好时机,以便更轻松地处理后面的关卡。
按需治疗
当你死亡的时候感觉很糟糕,特别是当你的本意只是想看看你的新关卡设计!所以我们将添加一个新的作弊选项:治疗。打开 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; } }
完成这些后,你只需按两次键就可以在需要时获得免费治疗!这应该使探索我们后面的关卡变得更容易:
显示全部和上帝模式
另一个方便的功能是显示地图,特别是当您只想验证地图构建时。完全关闭死亡也是确保地图都在您认为应该在的位置的好方法!所以首先,我们将添加另外两个菜单项及其处理程序:
#![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.rs
和 raws/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>(); } ... }
现在您可以随时显示地图,并关闭承受伤害的能力:
这使得处理后期内容 容易得多,而无需一遍又一遍地玩(不过,最好时不时地玩一下,并找出错误)。
深邃洞穴基本布局
深邃洞穴仍然应该看起来很自然,但也应该有一个中心区域,地精类生物可以在其中扎营。我们在上一章中研究过的扩散限制聚集 (Diffusion-Limited Aggregation) 算法,特别是“中心吸引子 (central attractor)”模式,为基本布局提供了我们想要的几乎所有东西:
在 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
,您将看到要塞已就位(我在图形中使用的是上帝模式):
所以 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); } }
这样就好多了:
更多生成物
让我们花一点时间为关卡引入更好的盔甲和武器,并使其有可能生成。玩家开始面临真正的挑战,因此他们需要一些可能的改进!我们将首先将锁子甲添加到 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.json
的 spawn_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。