为玩家装备物品


关于本教程

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

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

Hands-On Rust


现在我们有了一个难度不断增加的地下城,是时候开始为玩家提供一些提高他们能力的方法了! 在本章中,我们将从最基本的人类任务开始:装备武器和盾牌。

添加一些可以穿戴/挥舞的物品

我们已经有了很多物品系统的基础,所以我们将基于前几章的基础继续构建。 仅使用我们已经拥有的组件,我们可以在 spawners.rs 中从以下内容开始:

#![allow(unused)]
fn main() {
fn dagger(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('/'),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Dagger".to_string() })
        .with(Item{})
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('('),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Shield".to_string() })
        .with(Item{})
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}
}

在这两种情况下,我们都在创建一个新实体。 我们给它一个 Position,因为它必须从地图上的某个位置开始。 我们分配一个 Renderable,设置为适当的 CP437/ASCII 字形。 我们给它们一个名称,并将它们标记为物品。 我们可以像这样将它们添加到生成表 (spawn table) 中:

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

我们也可以很容易地将它们包含在实际生成它们的系统中:

#![allow(unused)]
fn main() {
// 实际生成怪物
for spawn in spawn_points.iter() {
    let x = (*spawn.0 % MAPWIDTH) as i32;
    let y = (*spawn.0 / MAPWIDTH) as i32;

    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),
        _ => {}
    }
}
}

如果你现在 cargo run 项目,你可以四处奔跑并最终找到匕首或盾牌。 在你测试时,你可以考虑将生成频率从 3 提高到一个非常大的数字! 由于我们添加了 Item 标签,你可以在找到这些物品时捡起和放下它们。

Screenshot

装备物品

如果你不能使用匕首和盾牌,它们就不是很有用! 所以让我们让它们可装备。

Equippable 组件

我们需要一种方法来指示物品可以被装备。 你可能已经猜到了,但我们添加了一个新组件! 在 components.rs 中,我们添加:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum EquipmentSlot { Melee, Shield }

#[derive(Component, Serialize, Deserialize, Clone)]
pub struct Equippable {
    pub slot : EquipmentSlot
}
}

既然我们有了序列化支持(来自第 11 章),我们也必须记住在几个地方注册它。 在 main.rs 中,我们将其添加到已注册组件的列表中:

#![allow(unused)]
fn main() {
gs.ecs.register::<Equippable>();
}

saveload_system.rs 中,我们将其添加到两组组件列表中:

#![allow(unused)]
fn main() {
serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster,
    Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
    AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
    WantsToDropItem, SerializationHelper, Equippable
);
}
#![allow(unused)]
fn main() {
deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster,
    Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
    AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
    WantsToDropItem, SerializationHelper, Equippable
);
}

最后,我们应该将 Equippable 组件添加到 spawners.rs 中的 daggershield 函数中:

#![allow(unused)]
fn main() {
fn dagger(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('/'),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Dagger".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Melee })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('('),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Shield".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Shield })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}
}

使物品可装备

一般来说,背包里放着盾牌没有太大帮助(先不考虑明显的“你是怎么把它塞进去的?”问题 - 像许多游戏一样,我们将忽略这个问题!) - 所以你必须能够选择一个来装备。 我们将首先制作另一个组件 Equipped。 它的工作方式与 InBackpack 类似 - 它指示实体正在持有它。 与 InBackpack 不同,它将指示正在使用的槽位。 这是 components.rs 中的基本 Equipped 组件:

#![allow(unused)]
fn main() {
#[derive(Component, ConvertSaveload, Clone)]
pub struct Equipped {
    pub owner : Entity,
    pub slot : EquipmentSlot
}
}

与之前一样,我们需要在 main.rs 中注册它,并将其包含在 saveload_system.rs 中的序列化和反序列化列表中。

实际装备物品

现在我们想让装备物品成为可能。 这样做将自动卸下同一槽位中的任何物品。 我们将通过我们已经用于使用物品的相同接口来完成此操作,这样我们就不会到处都有不同的菜单。 打开 inventory_system.rs,我们将编辑 ItemUseSystem。 我们将首先扩展我们正在引用的系统列表:

#![allow(unused)]
fn main() {
impl<'a> System<'a> for ItemUseSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( ReadExpect<'a, Entity>,
                        WriteExpect<'a, GameLog>,
                        ReadExpect<'a, Map>,
                        Entities<'a>,
                        WriteStorage<'a, WantsToUseItem>,
                        ReadStorage<'a, Name>,
                        ReadStorage<'a, Consumable>,
                        ReadStorage<'a, ProvidesHealing>,
                        ReadStorage<'a, InflictsDamage>,
                        WriteStorage<'a, CombatStats>,
                        WriteStorage<'a, SufferDamage>,
                        ReadStorage<'a, AreaOfEffect>,
                        WriteStorage<'a, Confusion>,
                        ReadStorage<'a, Equippable>,
                        WriteStorage<'a, Equipped>,
                        WriteStorage<'a, InBackpack>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (player_entity, mut gamelog, map, entities, mut wants_use, names,
            consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage,
            aoe, mut confused, equippable, mut equipped, mut backpack) = data;
}

现在,在获取目标之后,添加以下代码块:

#![allow(unused)]
fn main() {
// 如果物品是可装备的,那么我们想要装备它 - 并卸下该槽位中的任何其他物品
let item_equippable = equippable.get(useitem.item);
match item_equippable {
    None => {}
    Some(can_equip) => {
        let target_slot = can_equip.slot;
        let target = targets[0];

        // 移除目标在物品槽位中已有的任何物品
        let mut to_unequip : Vec<Entity> = Vec::new();
        for (item_entity, already_equipped, name) in (&entities, &equipped, &names).join() {
            if already_equipped.owner == target && already_equipped.slot == target_slot {
                to_unequip.push(item_entity);
                if target == *player_entity {
                    gamelog.entries.push(format!("You unequip {}.", name.name));
                }
            }
        }
        for item in to_unequip.iter() {
            equipped.remove(*item);
            backpack.insert(*item, InBackpack{ owner: target }).expect("Unable to insert backpack entry");
        }

        // 挥舞物品
        equipped.insert(useitem.item, Equipped{ owner: target, slot: target_slot }).expect("Unable to insert equipped component");
        backpack.remove(useitem.item);
        if target == *player_entity {
            gamelog.entries.push(format!("You equip {}.", names.get(useitem.item).unwrap().name));
        }
    }
}
}

这首先匹配以查看我们是否可以装备该物品。 如果可以,它会查找该物品的目标槽位,并查看该槽位中是否已存在物品。 如果有,则将其移动到背包中。 最后,它将 Equipped 组件添加到物品实体,其中包含所有者(现在是玩家)和适当的槽位。

最后,你可能还记得,当玩家移动到下一层时,我们会删除很多实体。 我们希望将玩家 Equipped 的物品作为保留 ECS 中物品的理由。 在 main.rs 中,我们修改 entities_to_remove_on_level_change 如下:

#![allow(unused)]
fn main() {
fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> {
    let entities = self.ecs.entities();
    let player = self.ecs.read_storage::<Player>();
    let backpack = self.ecs.read_storage::<InBackpack>();
    let player_entity = self.ecs.fetch::<Entity>();
    let equipped = self.ecs.read_storage::<Equipped>();

    let mut to_delete : Vec<Entity> = Vec::new();
    for entity in entities.join() {
        let mut should_delete = true;

        // 不要删除玩家
        let p = player.get(entity);
        if let Some(_p) = p {
            should_delete = false;
        }

        // 不要删除玩家的装备
        let bp = backpack.get(entity);
        if let Some(bp) = bp {
            if bp.owner == *player_entity {
                should_delete = false;
            }
        }

        let eq = equipped.get(entity);
        if let Some(eq) = eq {
            if eq.owner == *player_entity {
                should_delete = false;
            }
        }

        if should_delete {
            to_delete.push(entity);
        }
    }

    to_delete
}
}

如果你现在 cargo run 项目,你可以四处奔跑捡起新物品 - 并且可以装备它们。 它们目前还没有任何作用 - 但至少你可以换入和换出它们。 游戏日志将显示装备和卸下装备。

Screenshot

授予战斗奖励

从逻辑上讲,盾牌应该提供一些针对传入伤害的保护 - 而被匕首刺伤应该比被拳头击中更疼! 为了方便这一点,我们将添加更多组件(现在应该是一首熟悉的歌了)。 在 components.rs 中:

#![allow(unused)]
fn main() {
#[derive(Component, ConvertSaveload, Clone)]
pub struct MeleePowerBonus {
    pub power : i32
}

#[derive(Component, ConvertSaveload, Clone)]
pub struct DefenseBonus {
    pub defense : i32
}
}

我们还需要记住在 main.rssaveload_system.rs 中注册它们。 然后我们可以修改 spawner.rs 中的代码,将这些组件添加到正确的物品中:

#![allow(unused)]
fn main() {
fn dagger(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('/'),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Dagger".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Melee })
        .with(MeleePowerBonus{ power: 2 })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}

fn shield(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('('),
            fg: RGB::named(rltk::CYAN),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Shield".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Shield })
        .with(DefenseBonus{ defense: 1 })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}
}

请注意我们是如何将组件添加到每个物品的? 现在我们需要修改 melee_combat_system 以应用这些奖励。 我们通过向我们的系统添加一些额外的 ECS 查询来做到这一点:

#![allow(unused)]
fn main() {
impl<'a> System<'a> for MeleeCombatSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( Entities<'a>,
                        WriteExpect<'a, GameLog>,
                        WriteStorage<'a, WantsToMelee>,
                        ReadStorage<'a, Name>,
                        ReadStorage<'a, CombatStats>,
                        WriteStorage<'a, SufferDamage>,
                        ReadStorage<'a, MeleePowerBonus>,
                        ReadStorage<'a, DefenseBonus>,
                        ReadStorage<'a, Equipped>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, melee_power_bonuses, defense_bonuses, equipped) = data;

        for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() {
            if stats.hp > 0 {
                let mut offensive_bonus = 0;
                for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() {
                    if equipped_by.owner == entity {
                        offensive_bonus += power_bonus.power;
                    }
                }

                let target_stats = combat_stats.get(wants_melee.target).unwrap();
                if target_stats.hp > 0 {
                    let target_name = names.get(wants_melee.target).unwrap();

                    let mut defensive_bonus = 0;
                    for (_item_entity, defense_bonus, equipped_by) in (&entities, &defense_bonuses, &equipped).join() {
                        if equipped_by.owner == wants_melee.target {
                            defensive_bonus += defense_bonus.defense;
                        }
                    }

                    let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus));
}

这是一大段代码,所以让我们过一遍:

  1. 我们已将 MeleePowerBonusDefenseBonusEquipped 读取器添加到系统中。
  2. 一旦我们确定攻击者还活着,我们将 offensive_bonus 设置为 0。
  3. 我们迭代所有具有 MeleePowerBonusEquipped 条目的实体。 如果它们是由攻击者装备的,我们将它们的攻击奖励添加到 offensive_bonus
  4. 一旦我们确定防御者还活着,我们将 defensive_bonus 设置为 0。
  5. 我们迭代所有具有 DefenseBonusEquipped 条目的实体。 如果它们是由目标装备的,我们将它们的防御添加到 defense_bonus
  6. 当我们计算伤害时,我们将攻击奖励添加到攻击方 - 并将防御奖励添加到防御方。

如果你现在 cargo run,你会发现使用匕首会让你攻击更猛烈 - 而使用盾牌会让你受到的伤害更少。

卸下物品

现在你可以装备物品,并通过交换来移除它们,你可能想要停止持有物品并将其放回背包。 在像这样简单的游戏中,这并不是绝对必要的 - 但这是一个很好的未来选择。 我们将 R 键绑定到 remove 物品,因为该键是可用的。 在 player.rs 中,将此添加到输入代码:

#![allow(unused)]
fn main() {
VirtualKeyCode::R => return RunState::ShowRemoveItem,
}

现在我们在 main.rsRunState 中添加 ShowRemoveItem

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState { AwaitingInput,
    PreRun,
    PlayerTurn,
    MonsterTurn,
    ShowInventory,
    ShowDropItem,
    ShowTargeting { range : i32, item : Entity},
    MainMenu { menu_selection : gui::MainMenuSelection },
    SaveGame,
    NextLevel,
    ShowRemoveItem,
    GameOver
}
}

我们在 tick 中为其添加一个处理程序:

#![allow(unused)]
fn main() {
RunState::ShowRemoveItem => {
    let result = gui::remove_item_menu(self, ctx);
    match result.0 {
        gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
        gui::ItemMenuResult::NoResponse => {}
        gui::ItemMenuResult::Selected => {
            let item_entity = result.1.unwrap();
            let mut intent = self.ecs.write_storage::<WantsToRemoveItem>();
            intent.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem{ item: item_entity }).expect("Unable to insert intent");
            newrunstate = RunState::PlayerTurn;
        }
    }
}
}

我们将在 components.rs 中实现一个新组件(有关序列化处理程序,请参阅源代码;它是想要丢弃物品的处理程序的剪切粘贴,并更改了名称):

#![allow(unused)]
fn main() {
#[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct WantsToRemoveItem {
    pub item : Entity
}
}

与往常一样,它必须在 main.rssaveload_system.rs 中注册。

现在在 gui.rs 中,我们将实现 remove_item_menu。 它几乎与物品丢弃菜单完全相同,但更改了查询内容和标题(在某个时候将这些变成更通用的函数将是一个好主意!):

#![allow(unused)]
fn main() {
pub fn remove_item_menu(gs : &mut State, ctx : &mut Rltk) -> (ItemMenuResult, Option<Entity>) {
    let player_entity = gs.ecs.fetch::<Entity>();
    let names = gs.ecs.read_storage::<Name>();
    let backpack = gs.ecs.read_storage::<Equipped>();
    let entities = gs.ecs.entities();

    let inventory = (&backpack, &names).join().filter(|item| item.0.owner == *player_entity );
    let count = inventory.count();

    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), "Remove Which Item?");
    ctx.print_color(18, y+count as i32+1, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "ESCAPE to cancel");

    let mut equippable : Vec<Entity> = Vec::new();
    let mut j = 0;
    for (entity, _pack, name) in (&entities, &backpack, &names).join().filter(|item| item.1.owner == *player_entity ) {
        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), 97+j as rltk::FontCharType);
        ctx.set(19, y, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), rltk::to_cp437(')'));

        ctx.print(21, y, &name.name.to_string());
        equippable.push(entity);
        y += 1;
        j += 1;
    }

    match ctx.key {
        None => (ItemMenuResult::NoResponse, None),
        Some(key) => {
            match key {
                VirtualKeyCode::Escape => { (ItemMenuResult::Cancel, None) }
                _ => {
                    let selection = rltk::letter_to_option(key);
                    if selection > -1 && selection < count as i32 {
                        return (ItemMenuResult::Selected, Some(equippable[selection as usize]));
                    }
                    (ItemMenuResult::NoResponse, None)
                }
            }
        }
    }
}
}

接下来,我们应该扩展 inventory_system.rs 以支持移除物品。 幸运的是,这是一个非常简单的系统:

#![allow(unused)]
fn main() {
pub struct ItemRemoveSystem {}

impl<'a> System<'a> for ItemRemoveSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = (
                        Entities<'a>,
                        WriteStorage<'a, WantsToRemoveItem>,
                        WriteStorage<'a, Equipped>,
                        WriteStorage<'a, InBackpack>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut wants_remove, mut equipped, mut backpack) = data;

        for (entity, to_remove) in (&entities, &wants_remove).join() {
            equipped.remove(to_remove.item);
            backpack.insert(to_remove.item, InBackpack{ owner: entity }).expect("Unable to insert backpack");
        }

        wants_remove.clear();
    }
}
}

最后,我们将其添加到 main.rs 中的系统中:

#![allow(unused)]
fn main() {
impl State {
    fn run_systems(&mut self) {
        let mut vis = VisibilitySystem{};
        vis.run_now(&self.ecs);
        let mut mob = MonsterAI{};
        mob.run_now(&self.ecs);
        let mut mapindex = MapIndexingSystem{};
        mapindex.run_now(&self.ecs);
        let mut melee = MeleeCombatSystem{};
        melee.run_now(&self.ecs);
        let mut damage = DamageSystem{};
        damage.run_now(&self.ecs);
        let mut pickup = ItemCollectionSystem{};
        pickup.run_now(&self.ecs);
        let mut itemuse = ItemUseSystem{};
        itemuse.run_now(&self.ecs);
        let mut drop_items = ItemDropSystem{};
        drop_items.run_now(&self.ecs);
        let mut item_remove = ItemRemoveSystem{};
        item_remove.run_now(&self.ecs);

        self.ecs.maintain();
    }
}
}

现在,如果你 cargo run,你可以捡起匕首或盾牌并装备它。 然后你可以按 R 来移除它。

稍后添加更强大的装备

让我们在 spawner.rs 中添加更多物品:

#![allow(unused)]
fn main() {
fn longsword(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('/'),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Longsword".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Melee })
        .with(MeleePowerBonus{ power: 4 })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}

fn tower_shield(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('('),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
            render_order: 2
        })
        .with(Name{ name : "Tower Shield".to_string() })
        .with(Item{})
        .with(Equippable{ slot: EquipmentSlot::Shield })
        .with(DefenseBonus{ defense: 3 })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();
}
}

我们将在 random_table.rs 中添加一个快速修复,以忽略生成几率 (spawn chance) 为 0 或更低的条目:

#![allow(unused)]
fn main() {
pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable {
    if weight > 0 {
        self.total_weight += weight;
        self.entries.push(RandomEntry::new(name.to_string(), weight));
    }
    self
}
}

回到 spawner.rs,我们将它们添加到战利品表 (loot table) 中 - 并在地下城后期有一定几率出现:

#![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)
}
}
#![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),
    _ => {}
}
}

现在,随着你进一步深入,你可以找到更好的武器和盾牌!

游戏结束画面

我们快要完成基本教程了,所以让我们在您死亡时做一些事情 - 而不是锁定在控制台循环中。 在文件 damage_system.rs 中,我们将编辑 delete_the_deadplayer 的 match 语句:

#![allow(unused)]
fn main() {
match player {
    None => {
        let victim_name = names.get(entity);
        if let Some(victim_name) = victim_name {
            log.entries.push(format!("{} is dead", &victim_name.name));
        }
        dead.push(entity)
    }
    Some(_) => {
        let mut runstate = ecs.write_resource::<RunState>();
        *runstate = RunState::GameOver;
    }
}
}

当然,我们现在必须去 main.rs 并添加新状态:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState { AwaitingInput,
    PreRun,
    PlayerTurn,
    MonsterTurn,
    ShowInventory,
    ShowDropItem,
    ShowTargeting { range : i32, item : Entity},
    MainMenu { menu_selection : gui::MainMenuSelection },
    SaveGame,
    NextLevel,
    ShowRemoveItem,
    GameOver
}
}

我们将其添加到状态实现中,也在 main.rs 中:

#![allow(unused)]
fn main() {
RunState::GameOver => {
    let result = gui::game_over(ctx);
    match result {
        gui::GameOverResult::NoSelection => {}
        gui::GameOverResult::QuitToMenu => {
            self.game_over_cleanup();
            newrunstate = RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame };
        }
    }
}
}

这相对简单:我们调用 game_over 来渲染菜单,当您退出时,我们删除 ECS 中的所有内容。 最后,在 gui.rs 中,我们将实现 game_over

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum GameOverResult { NoSelection, QuitToMenu }

pub fn game_over(ctx : &mut Rltk) -> GameOverResult {
    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Your journey has ended!");
    ctx.print_color_centered(17, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "One day, we'll tell you all about how you did.");
    ctx.print_color_centered(18, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter..");

    ctx.print_color_centered(20, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Press any key to return to the menu.");

    match ctx.key {
        None => GameOverResult::NoSelection,
        Some(_) => GameOverResult::QuitToMenu
    }
}
}

最后,我们将处理 game_over_cleanup

#![allow(unused)]
fn main() {
fn game_over_cleanup(&mut self) {
    // 删除所有内容
    let mut to_delete = Vec::new();
    for e in self.ecs.entities().join() {
        to_delete.push(e);
    }
    for del in to_delete.iter() {
        self.ecs.delete_entity(*del).expect("Deletion failed");
    }

    // 构建新地图并放置玩家
    let worldmap;
    {
        let mut worldmap_resource = self.ecs.write_resource::<Map>();
        *worldmap_resource = Map::new_map_rooms_and_corridors(1);
        worldmap = worldmap_resource.clone();
    }

    // 生成坏人
    for room in worldmap.rooms.iter().skip(1) {
        spawner::spawn_room(&mut self.ecs, room, 1);
    }

    // 放置玩家并更新资源
    let (player_x, player_y) = worldmap.rooms[0].center();
    let player_entity = spawner::player(&mut self.ecs, player_x, player_y);
    let mut player_position = self.ecs.write_resource::<Point>();
    *player_position = Point::new(player_x, player_y);
    let mut position_components = self.ecs.write_storage::<Position>();
    let mut player_entity_writer = self.ecs.write_resource::<Entity>();
    *player_entity_writer = player_entity;
    let player_pos_comp = position_components.get_mut(player_entity);
    if let Some(player_pos_comp) = player_pos_comp {
        player_pos_comp.x = player_x;
        player_pos_comp.y = player_y;
    }

    // 将玩家的视野标记为脏 (dirty)
    let mut viewshed_components = self.ecs.write_storage::<Viewshed>();
    let vs = viewshed_components.get_mut(player_entity);
    if let Some(vs) = vs {
        vs.dirty = true;
    }
}
}

从我们加载游戏时的序列化工作中,这应该看起来很熟悉。 它非常相似,但它生成了一个新玩家。

现在,如果你 cargo run,并且死亡 - 你将收到一条消息,告知你游戏结束,并将你送回菜单。

Screenshot

总结

这是本教程第一部分的结尾。 它相对紧密地遵循了 Python 教程,并将您从“hello rust”带到一个相当有趣的 Roguelike 游戏。 我希望你喜欢它! 请继续关注,我希望很快添加第二部分。

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

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


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