好的,请看下面翻译后的文档:
简易陷阱
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
大多数 Roguelike 游戏,就像它们的前身《龙与地下城》(D&D) 一样,在地牢中都设有陷阱。走在看起来无辜的走廊上,哎哟 - 一支箭飞出来击中你。 本章将实现一些简单的陷阱,然后研究它们带来的游戏影响。
什么是陷阱?
大多数陷阱都遵循以下模式:你可能会看到陷阱(也可能看不到!),你无论如何都进入了该图块,陷阱触发,然后发生一些事情(伤害、传送等)。 因此,陷阱可以逻辑地分为三个部分:
- 外观(我们已经支持了),可能会被发现,也可能不会被发现(我们尚未实现)。
- 触发器 - 如果你进入陷阱的图块,就会发生一些事情。
- 效果 - 我们已经在魔法物品中接触过了。
让我们逐步完成这些组件的实现。
渲染一个基本的捕熊陷阱
许多 Roguelike 游戏使用 ^
作为陷阱的符号,所以我们也将这样做。 我们拥有渲染基本对象所需的所有组件,因此我们将创建一个新的生成函数(在 spawners.rs
中)。 它几乎是将字形放在地图上的最简实现:
#![allow(unused)] fn main() { fn bear_trap(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('^'), fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Bear Trap".to_string() }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
我们还将它添加到可以生成的事物列表中:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1 + map_depth) .add("Health Potion", 7) .add("Fireball Scroll", 2 + map_depth) .add("Confusion Scroll", 2 + map_depth) .add("Magic Missile Scroll", 4) .add("Dagger", 3) .add("Shield", 3) .add("Longsword", map_depth - 1) .add("Tower Shield", map_depth - 1) .add("Rations", 10) .add("Magic Mapping Scroll", 2) .add("Bear Trap", 2) } }
#![allow(unused)] fn main() { match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y), "Orc" => orc(ecs, x, y), "Health Potion" => health_potion(ecs, x, y), "Fireball Scroll" => fireball_scroll(ecs, x, y), "Confusion Scroll" => confusion_scroll(ecs, x, y), "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y), "Dagger" => dagger(ecs, x, y), "Shield" => shield(ecs, x, y), "Longsword" => longsword(ecs, x, y), "Tower Shield" => tower_shield(ecs, x, y), "Rations" => rations(ecs, x, y), "Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y), "Bear Trap" => bear_trap(ecs, x, y), _ => {} } }
如果你现在 cargo run
运行项目,偶尔你会遇到一个红色的 ^
- 并且鼠标悬停时它会被标记为 “Bear Trap”(捕熊陷阱)。 不是很令人兴奋,但这是一个好的开始! 请注意,为了测试,我们将生成频率从 2 提高到 100 - 大量陷阱,使调试更容易。 记住稍后降低它!
但你并不总是能发现陷阱!
如果你总是知道陷阱在等着你,那就太容易了! 所以我们希望默认情况下使陷阱是隐藏的,并想出一种在靠近陷阱时有时可以定位陷阱的方法。 就像 ECS 驱动世界中的大多数事物一样,分析文本可以很好地提示您需要哪些组件。 在这种情况下,我们需要进入 components.rs
并创建一个新组件 - Hidden
:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Hidden {} }
像往常一样,我们需要在 main.rs
和 saveload_system.rs
中注册它。 我们还将该属性赋予新的捕熊陷阱:
#![allow(unused)] fn main() { fn bear_trap(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('^'), fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Bear Trap".to_string() }) .with(Hidden{}) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
现在,我们想要修改对象渲染器,使其不显示隐藏的事物。 Specs Book 为如何从连接 (join) 中排除组件提供了很好的线索,所以我们这样做(在 main.rs
中):
#![allow(unused)] fn main() { let mut data = (&positions, &renderables, !&hidden).join().collect::<Vec<_>>(); data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) ); for (pos, render, _hidden) in data.iter() { let idx = map.xy_idx(pos.x, pos.y); if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) } } }
请注意,我们在连接中添加了一个 !
(“非” 符号)- 我们表示如果我们要渲染实体,则实体不能具有 Hidden
组件。
如果你现在 cargo run
运行项目,捕熊陷阱将不再可见。 但是,它们会显示在工具提示中(也许这样也很好,我们知道它们在那里!)。 我们也将它们从工具提示中排除。 在 gui.rs
中,我们修改 draw_tooltips
函数:
#![allow(unused)] fn main() { fn draw_tooltips(ecs: &World, ctx : &mut Rltk) { let map = ecs.fetch::<Map>(); let names = ecs.read_storage::<Name>(); let positions = ecs.read_storage::<Position>(); let hidden = ecs.read_storage::<Hidden>(); let mouse_pos = ctx.mouse_pos(); if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { return; } let mut tooltip : Vec<String> = Vec::new(); for (name, position, _hidden) in (&names, &positions, !&hidden).join() { let idx = map.xy_idx(position.x, position.y); if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] { tooltip.push(name.name.to_string()); } } ... }
现在,如果你 cargo run
运行项目,你将不知道陷阱的存在。 因为它们还没有做任何事情 - 它们可能根本不存在!
添加进入触发器
当实体走到陷阱上时,陷阱应该被触发。 因此,在 components.rs
中,我们将创建一个 EntryTrigger
(像往常一样,我们也会在 main.rs
和 saveload_system.rs
中注册它):
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct EntryTrigger {} }
我们将给捕熊陷阱一个触发器(在 spawner.rs
中):
#![allow(unused)] fn main() { fn bear_trap(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('^'), fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Bear Trap".to_string() }) .with(Hidden{}) .with(EntryTrigger{}) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
我们还需要让陷阱在实体进入时触发其触发器。 我们将添加另一个组件 EntityMoved
来指示实体在本回合已移动。 在 components.rs
中(并记住在 main.rs
和 saveload_system.rs
中注册):
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct EntityMoved {} }
现在,我们搜索代码库,在每次实体移动时添加一个 EntityMoved
组件。 在 player.rs
中,我们在 try_move_player
函数中处理玩家移动。 在顶部,我们将获得对相关组件存储的写入访问权:
#![allow(unused)] fn main() { let mut entity_moved = ecs.write_storage::<EntityMoved>(); }
然后,当我们确定玩家确实移动了时 - 我们将插入 EntityMoved
组件:
#![allow(unused)] fn main() { entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); }
另一个具有移动功能的位置是怪物 AI。 因此,在 monster_ai_system.rs
中,我们执行类似的操作。 我们为 EntityMoved
组件添加一个 WriteResource
,并在怪物移动后插入一个。 AI 的源代码有点长,所以我建议您直接查看源文件 (here) 以了解此部分。
最后,我们需要一个系统来使触发器实际做一些事情。 我们将创建一个新文件 trigger_system.rs
:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{EntityMoved, Position, EntryTrigger, Hidden, Map, Name, gamelog::GameLog}; pub struct TriggerSystem {} impl<'a> System<'a> for TriggerSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Map>, WriteStorage<'a, EntityMoved>, ReadStorage<'a, Position>, ReadStorage<'a, EntryTrigger>, WriteStorage<'a, Hidden>, ReadStorage<'a, Name>, Entities<'a>, WriteExpect<'a, GameLog>); fn run(&mut self, data : Self::SystemData) { let (map, mut entity_moved, position, entry_trigger, mut hidden, names, entities, mut log) = data; // 迭代移动的实体及其最终位置 for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { let idx = map.xy_idx(pos.x, pos.y); for entity_id in map.tile_content[idx].iter() { if entity != *entity_id { // 不要费心检查自己是否是陷阱! let maybe_trigger = entry_trigger.get(*entity_id); match maybe_trigger { None => {}, Some(_trigger) => { // 我们触发了它 let name = names.get(*entity_id); if let Some(name) = name { log.entries.push(format!("{} triggers!", &name.name)); } hidden.remove(*entity_id); // 陷阱不再隐藏 } } } } } // 移除所有实体移动标记 entity_moved.clear(); } } }
如果您已经完成了前面的章节,这相对简单:
- 我们迭代所有具有
Position
和EntityMoved
组件的实体。 - 我们获取它们位置的地图索引。
- 我们迭代
tile_content
索引以查看新图块中的内容。 - 我们查看那里是否有陷阱。
- 如果有,我们获取其名称并通过日志通知玩家陷阱已激活。
- 我们从陷阱中移除
hidden
组件,因为我们现在知道它在那里。
我们还必须进入 main.rs
并插入代码以运行系统。 它在怪物 AI 之后运行,因为怪物可以移动 - 但我们可能会输出伤害,因此该系统需要稍后运行:
#![allow(unused)] fn main() { ... let mut mob = MonsterAI{}; mob.run_now(&self.ecs); let mut triggers = trigger_system::TriggerSystem{}; triggers.run_now(&self.ecs); ... }
造成伤害的陷阱
到此为止,我们已经走了很长一段路:陷阱可以散布在关卡周围,并在你进入其目标图块时触发。 如果陷阱做一些事情,那将会有所帮助! 我们实际上有相当多的组件类型来描述效果。 在 spawner.rs
中,我们将扩展捕熊陷阱以包含一些伤害:
#![allow(unused)] fn main() { fn bear_trap(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('^'), fg: RGB::named(rltk::RED), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Bear Trap".to_string() }) .with(Hidden{}) .with(EntryTrigger{}) .with(InflictsDamage{ damage: 6 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
我们还将扩展 trigger_system
以应用伤害:
#![allow(unused)] fn main() { // 如果陷阱是造成伤害的,则执行它 let damage = inflicts_damage.get(*entity_id); if let Some(damage) = damage { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); SufferDamage::new_damage(&mut inflict_damage, entity, damage.damage); } }
如果你现在 cargo run
运行项目,你可以四处移动 - 并且走进陷阱会伤害你。 如果怪物走进陷阱,它也会伤害它们! 它甚至播放攻击的粒子效果。
捕熊陷阱只能触发一次
有些陷阱,例如捕熊陷阱(想想带有尖刺的弹簧),实际上只能触发一次。 这似乎是我们触发器系统建模的一个有用属性,因此我们将添加一个新组件(到 components.rs
,main.rs
和 saveload_system.rs
):
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct SingleActivation {} }
我们还将它添加到 spawner.rs
中的 Bear Trap 函数中:
#![allow(unused)] fn main() { .with(SingleActivation{}) }
现在我们修改 trigger_system
以应用它。 请注意,我们在循环遍历实体之后删除它们,以避免混淆迭代器。
#![allow(unused)] fn main() { use specs::prelude::*; use super::{EntityMoved, Position, EntryTrigger, Hidden, Map, Name, gamelog::GameLog, InflictsDamage, particle_system::ParticleBuilder, SufferDamage, SingleActivation}; pub struct TriggerSystem {} impl<'a> System<'a> for TriggerSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Map>, WriteStorage<'a, EntityMoved>, ReadStorage<'a, Position>, ReadStorage<'a, EntryTrigger>, WriteStorage<'a, Hidden>, ReadStorage<'a, Name>, Entities<'a>, WriteExpect<'a, GameLog>, ReadStorage<'a, InflictsDamage>, WriteExpect<'a, ParticleBuilder>, WriteStorage<'a, SufferDamage>, ReadStorage<'a, SingleActivation>); fn run(&mut self, data : Self::SystemData) { let (map, mut entity_moved, position, entry_trigger, mut hidden, names, entities, mut log, inflicts_damage, mut particle_builder, mut inflict_damage, single_activation) = data; // 迭代移动的实体及其最终位置 let mut remove_entities : Vec<Entity> = Vec::new(); for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { let idx = map.xy_idx(pos.x, pos.y); for entity_id in map.tile_content[idx].iter() { if entity != *entity_id { // 不要费心检查自己是否是陷阱! let maybe_trigger = entry_trigger.get(*entity_id); match maybe_trigger { None => {}, Some(_trigger) => { // 我们触发了它 let name = names.get(*entity_id); if let Some(name) = name { log.entries.push(format!("{} triggers!", &name.name)); } hidden.remove(*entity_id); // 陷阱不再隐藏 // 如果陷阱是造成伤害的,则执行它 let damage = inflicts_damage.get(*entity_id); if let Some(damage) = damage { particle_builder.request(pos.x, pos.y, rltk::RGB::named(rltk::ORANGE), rltk::RGB::named(rltk::BLACK), rltk::to_cp437('‼'), 200.0); SufferDamage::new_damage(&mut inflict_damage, entity, damage.damage); } // 如果是单次激活,则需要将其移除 let sa = single_activation.get(*entity_id); if let Some(_sa) = sa { remove_entities.push(*entity_id); } } } } } } // 移除任何单次激活陷阱 for trap in remove_entities.iter() { entities.delete(*trap).expect("Unable to delete trap"); } // 移除所有实体移动标记 entity_moved.clear(); } } }
如果你现在 cargo run
运行项目(我建议 cargo run --release
- 它变得越来越慢!),你可能会被捕熊陷阱击中 - 受到一些伤害,然后陷阱消失。
发现陷阱
我们现在有了一个相当实用的陷阱系统,但是随机地无缘无故地受到伤害是很烦人的 - 因为你无法知道陷阱在那里。 这也很不公平,因为没有办法防范它。 我们将实现发现陷阱的机会。 在未来的某个时候,这可能会与属性或技能相关联 - 但就目前而言,我们将使用掷骰子。 这比要求每个人始终携带 10 英尺长的杆子要好一些(就像早期的 D&D 游戏!)。
由于 visibility_system
已经处理了显示图块,为什么不让它也可能显示隐藏的东西呢? 这是 visibility_system.rs
的代码:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Viewshed, Position, Map, Player, Hidden, gamelog::GameLog}; use rltk::{field_of_view, Point}; pub struct VisibilitySystem {} impl<'a> System<'a> for VisibilitySystem { #[allow(clippy::type_complexity)] type SystemData = ( WriteExpect<'a, Map>, Entities<'a>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Position>, ReadStorage<'a, Player>, WriteStorage<'a, Hidden>, WriteExpect<'a, rltk::RandomNumberGenerator>, WriteExpect<'a, GameLog>, ReadStorage<'a, Name>,); fn run(&mut self, data : Self::SystemData) { let (mut map, entities, mut viewshed, pos, player, mut hidden, mut rng, mut log, names) = data; for (ent,viewshed,pos) in (&entities, &mut viewshed, &pos).join() { if viewshed.dirty { viewshed.dirty = false; viewshed.visible_tiles = field_of_view(Point::new(pos.x, pos.y), viewshed.range, &*map); viewshed.visible_tiles.retain(|p| p.x >= 0 && p.x < map.width && p.y >= 0 && p.y < map.height ); // 如果这是玩家,则显示他们可以看到的东西 let _p : Option<&Player> = player.get(ent); if let Some(_p) = _p { for t in map.visible_tiles.iter_mut() { *t = false }; for vis in viewshed.visible_tiles.iter() { let idx = map.xy_idx(vis.x, vis.y); map.revealed_tiles[idx] = true; map.visible_tiles[idx] = true; // 有机会显示隐藏的事物 for e in map.tile_content[idx].iter() { let maybe_hidden = hidden.get(*e); if let Some(_maybe_hidden) = maybe_hidden { if rng.roll_dice(1,24)==1 { let name = names.get(*e); if let Some(name) = name { log.entries.push(format!("You spotted a {}.", &name.name)); } hidden.remove(*e); } } } } } } } } } }
那么为什么发现陷阱的几率是 1/24 呢? 我尝试了一下,直到感觉差不多合适为止。 1/6(我的首选)太好了。 由于您的视野在您移动时会更新,因此您在四处移动时有很高的机会发现陷阱。 就像游戏设计中的许多事情一样:有时你只需要玩玩,直到感觉合适为止!
如果你现在 cargo run
运行项目,你可以四处走动 - 有时会发现陷阱。 怪物不会显示陷阱,除非它们掉入陷阱。
本章的源代码可以在 这里 找到
在您的浏览器中使用 WebAssembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson.