好的,请看下面翻译后的文档:

简易陷阱


关于本教程

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

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

Hands-On Rust


大多数 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.rssaveload_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.rssaveload_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.rssaveload_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();
    }
}
}

如果您已经完成了前面的章节,这相对简单:

  1. 我们迭代所有具有 PositionEntityMoved 组件的实体。
  2. 我们获取它们位置的地图索引。
  3. 我们迭代 tile_content 索引以查看新图块中的内容。
  4. 我们查看那里是否有陷阱。
  5. 如果有,我们获取其名称并通过日志通知玩家陷阱已激活。
  6. 我们从陷阱中移除 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.rsmain.rssaveload_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.