物品和库存


关于本教程

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

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

实践中的 Rust


到目前为止,我们有地图、怪物和砸东西!没有捡拾物品的“谋杀霍博”体验,就不是完整的 roguelike 游戏。本章将添加一些基本物品到游戏中,以及拾取、使用和丢弃它们所需的用户界面元素。

思考编写项目

面向对象和实体组件系统之间的一个主要区别是,你不再考虑某个东西位于继承树上,而是考虑它如何从组件组合而成。理想情况下,你已经有一些组件可以随时使用!

那么... 一个物品由什么组成?思考一下,可以说一个物品具有以下属性:

  • 它有一个 Renderable - 一种绘制它的方式。
  • 如果它在地上等待拾取 - 它有一个 Position
  • 如果它不在地上 - 比如在背包里,它需要一种方式来表明它被存储了。我们将从 InPack 开始。
  • 它是一个 item,这意味着它可以被拾取。所以它需要某种 Item 组件。
  • 如果它可以被使用,它需要某种方式来表明它可以被使用 - 以及如何使用它。

一致性随机

计算机实际上非常不擅长生成随机数。计算机本质上是确定性的——所以(不涉及加密内容)当你请求一个“随机”数时,你实际上得到的是一个“序列中非常难以预测的下一个数”。这个序列由一个种子控制——使用相同的种子,你总是得到相同的掷骰结果!

由于我们有越来越多的使用随机性的东西,让我们继续将 RNG(随机数生成器)作为一个资源。 在 main.rs 中,我们添加:

#![allow(unused)]
fn main() {
gs.ecs.insert(rltk::RandomNumberGenerator::new());
}

我们现在可以随时访问 RNG,而不必传递一个。由于我们没有创建新的 RNG,我们可以用种子启动它(我们会使用seeded而不是new,并提供一个种子)。我们稍后再担心这个问题;现在,它只是会让我们的代码更简洁!

改进的生成

每个房间一个怪物,总是在中间,使得游戏相当无聊。我们还需要支持生成物品以及怪物! 为此,我们将创建一个新文件 spawner.rs

#![allow(unused)]
fn main() {
use rltk::{ RGB, RandomNumberGenerator };
use specs::prelude::*;
use super::{CombatStats, Player, Renderable, Name, Position, Viewshed, Monster, BlocksTile};

/// Spawns the player and returns his/her entity object.
pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
    ecs
        .create_entity()
        .with(Position { x: player_x, y: player_y })
        .with(Renderable {
            glyph: rltk::to_cp437('@'),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
        })
        .with(Player{})
        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
        .with(Name{name: "Player".to_string() })
        .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
        .build()
}

/// Spawns a random monster at a given location
pub fn random_monster(ecs: &mut World, x: i32, y: i32) {
    let roll :i32;
    {
        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
        roll = rng.roll_dice(1, 2);
    }
    match roll {
        1 => { orc(ecs, x, y) }
        _ => { goblin(ecs, x, y) }
    }
}

fn orc(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('o'), "Orc"); }
fn goblin(ecs: &mut World, x: i32, y: i32) { monster(ecs, x, y, rltk::to_cp437('g'), "Goblin"); }

fn monster<S : ToString>(ecs: &mut World, x: i32, y: i32, glyph : rltk::FontCharType, name : S) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph,
            fg: RGB::named(rltk::RED),
            bg: RGB::named(rltk::BLACK),
        })
        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
        .with(Monster{})
        .with(Name{ name : name.to_string() })
        .with(BlocksTile{})
        .with(CombatStats{ max_hp: 16, hp: 16, defense: 1, power: 4 })
        .build();
}
}

如您所见,我们将 main.rs 中的现有代码包装在不同模块的函数中。我们不一定非要这样做,但这有助于保持整洁。由于我们将扩展生成功能,因此将它们分开是很好的。现在我们修改 main.rs 以使用它:

#![allow(unused)]
fn main() {
let player_entity = spawner::player(&mut gs.ecs, player_x, player_y);

gs.ecs.insert(rltk::RandomNumberGenerator::new());
for room in map.rooms.iter().skip(1) {
    let (x,y) = room.center();
    spawner::random_monster(&mut gs.ecs, x, y);
}
}

那确实更整洁了!cargo run 会给你我们在上一章结束时所拥有的东西。

生成所有事物

我们将扩展函数以在每个房间生成多个怪物,0 是一个选项。首先,我们将上一章中引入的 Map 常量改为公共的,以便在 spawner.rs 中使用:

#![allow(unused)]
fn main() {
pub const MAPWIDTH : usize = 80;
pub const MAPHEIGHT : usize = 43;
pub const MAPCOUNT : usize = MAPHEIGHT * MAPWIDTH;
}

我们希望控制我们生成的物品数量,包括怪物和物品。我们希望怪物比物品多,以避免出现“蒙蒂霍尔”地牢!另外,在 spawner.rs 中,我们将添加这些常量(它们可以放在任何地方,放在其他常量旁边是有意义的):

#![allow(unused)]
fn main() {
const MAX_MONSTERS : i32 = 4;
const MAX_ITEMS : i32 = 2;
}

仍在 spawner.rs 中,我们创建了一个新函数 - spawn_room,该函数使用这些常量:

#![allow(unused)]
fn main() {
/// Fills a room with stuff!
pub fn spawn_room(ecs: &mut World, room : &Rect) {
    let mut monster_spawn_points : Vec<usize> = Vec::new();

    // Scope to keep the borrow checker happy
    {
        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
        let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;

        for _i in 0 .. num_monsters {
            let mut added = false;
            while !added {
                let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
                let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
                let idx = (y * MAPWIDTH) + x;
                if !monster_spawn_points.contains(&idx) {
                    monster_spawn_points.push(idx);
                    added = true;
                }
            }
        }
    }

    // Actually spawn the monsters
    for idx in monster_spawn_points.iter() {
        let x = *idx % MAPWIDTH;
        let y = *idx / MAPWIDTH;
        random_monster(ecs, x as i32, y as i32);
    }
}
}

这获取了 RNG 和地图,并为应该生成多少怪物掷骰子。然后它会不断尝试添加未被占用的随机位置,直到生成足够数量的怪物。每个怪物随后在确定的位置生成。借用检查器对我们可以可变访问rng,然后传递 ECS 本身的想法并不满意:因此我们引入一个作用域以使其满意(在我们完成操作后自动放弃对 RNG 的访问)。

main.rs 中,我们将怪物生成器替换为:

#![allow(unused)]
fn main() {
for room in map.rooms.iter().skip(1) {
    spawner::spawn_room(&mut gs.ecs, room);
}
}

如果你现在cargo run这个项目,每个房间会有 0 到 4 个怪物。可能会有些棘手!

截图

健康药水实体

我们将通过在游戏中添加生命药水来提高一点生存机会!我们将首先添加一些组件来帮助定义药水。在 components.rs 中:

#![allow(unused)]
fn main() {
#[derive(Component, Debug)]
pub struct Item {}

#[derive(Component, Debug)]
pub struct Potion {
    pub heal_amount : i32
}
}

我们当然需要在 main.rs 中注册这些:

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

spawner.rs 中,我们将添加一个新函数:health_potion

#![allow(unused)]
fn main() {
fn health_potion(ecs: &mut World, x: i32, y: i32) {
    ecs.create_entity()
        .with(Position{ x, y })
        .with(Renderable{
            glyph: rltk::to_cp437('¡'),
            fg: RGB::named(rltk::MAGENTA),
            bg: RGB::named(rltk::BLACK),
        })
        .with(Name{ name : "Health Potion".to_string() })
        .with(Item{})
        .with(Potion{ heal_amount: 8 })
        .build();
}
}

这很简单:我们创建一个实体,包含一个位置、一个可渲染对象(我们选择了 ¡,因为它看起来有点像药水,而且我最喜欢的游戏《矮人要塞》也使用它)、一个名称、一个 Item 组件和一个 Potion 组件,该组件指定它可以治愈 8 点伤害。

现在我们可以修改生成器代码,使其有机会生成 0 到 2 个物品:

#![allow(unused)]
fn main() {
pub fn spawn_room(ecs: &mut World, room : &Rect) {
    let mut monster_spawn_points : Vec<usize> = Vec::new();
    let mut item_spawn_points : Vec<usize> = Vec::new();

    // Scope to keep the borrow checker happy
    {
        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
        let num_monsters = rng.roll_dice(1, MAX_MONSTERS + 2) - 3;
        let num_items = rng.roll_dice(1, MAX_ITEMS + 2) - 3;

        for _i in 0 .. num_monsters {
            let mut added = false;
            while !added {
                let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
                let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
                let idx = (y * MAPWIDTH) + x;
                if !monster_spawn_points.contains(&idx) {
                    monster_spawn_points.push(idx);
                    added = true;
                }
            }
        }

        for _i in 0 .. num_items {
            let mut added = false;
            while !added {
                let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize;
                let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize;
                let idx = (y * MAPWIDTH) + x;
                if !item_spawn_points.contains(&idx) {
                    item_spawn_points.push(idx);
                    added = true;
                }
            }
        }
    }

    // Actually spawn the monsters
    for idx in monster_spawn_points.iter() {
        let x = *idx % MAPWIDTH;
        let y = *idx / MAPWIDTH;
        random_monster(ecs, x as i32, y as i32);
    }

    // Actually spawn the potions
    for idx in item_spawn_points.iter() {
        let x = *idx % MAPWIDTH;
        let y = *idx / MAPWIDTH;
        health_potion(ecs, x as i32, y as i32);
    }
}
}

如果你现在cargo run这个项目,房间里有时会包含生命药水。工具提示和渲染“只需工作”——因为它们有使用所需组件。

截图

拾取物品

拥有药水是一个很好的开始,但如果能捡起它们就更好了!我们将在 components.rs 中创建一个新组件(并在 main.rs 中注册它!),以表示物品在某个人的背包中:

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

我们还希望使物品收集通用化——也就是说,任何实体都可以捡起物品。让它只适用于玩家会非常简单,但后来我们可能会决定怪物也可以捡起战利品(引入全新的战术元素——诱饵!)。因此,我们还会在components.rs中创建一个表示意图的组件(并在main.rs中注册它):

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

接下来,我们将组装一个处理WantsToPickupItem通知的系统。我们将创建一个新文件,inventory_system.rs

#![allow(unused)]
fn main() {
use specs::prelude::*;
use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog};

pub struct ItemCollectionSystem {}

impl<'a> System<'a> for ItemCollectionSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( ReadExpect<'a, Entity>,
                        WriteExpect<'a, GameLog>,
                        WriteStorage<'a, WantsToPickupItem>,
                        WriteStorage<'a, Position>,
                        ReadStorage<'a, Name>,
                        WriteStorage<'a, InBackpack>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack) = data;

        for pickup in wants_pickup.join() {
            positions.remove(pickup.item);
            backpack.insert(pickup.item, InBackpack{ owner: pickup.collected_by }).expect("Unable to insert backpack entry");

            if pickup.collected_by == *player_entity {
                gamelog.entries.push(format!("You pick up the {}.", names.get(pickup.item).unwrap().name));
            }
        }

        wants_pickup.clear();
    }
}
}

这会迭代请求以拾取物品,移除它们的位置组件,并添加一个分配给收集者的InBackpack组件。不要忘记将其添加到main.rs中的系统列表中:

#![allow(unused)]
fn main() {
let mut pickup = ItemCollectionSystem{};
pickup.run_now(&self.ecs);
}

下一步是添加一个输入命令来拾取物品。g 是常用的键,所以我们就用这个(随时可以更改!)。在 player.rs 中,在不断增长的输入 match 语句中,我们添加:

#![allow(unused)]
fn main() {
VirtualKeyCode::G => get_item(&mut gs.ecs),
}

你可能已经猜到了,下一步是实现 get_item

#![allow(unused)]
fn main() {
fn get_item(ecs: &mut World) {
    let player_pos = ecs.fetch::<Point>();
    let player_entity = ecs.fetch::<Entity>();
    let entities = ecs.entities();
    let items = ecs.read_storage::<Item>();
    let positions = ecs.read_storage::<Position>();
    let mut gamelog = ecs.fetch_mut::<GameLog>();    

    let mut target_item : Option<Entity> = None;
    for (item_entity, _item, position) in (&entities, &items, &positions).join() {
        if position.x == player_pos.x && position.y == player_pos.y {
            target_item = Some(item_entity);
        }
    }

    match target_item {
        None => gamelog.entries.push("There is nothing here to pick up.".to_string()),
        Some(item) => {
            let mut pickup = ecs.write_storage::<WantsToPickupItem>();
            pickup.insert(*player_entity, WantsToPickupItem{ collected_by: *player_entity, item }).expect("Unable to insert want to pickup");
        }
    }
}
}

这从 ECS 获取一堆引用/访问器,并迭代所有具有位置的项。如果它与玩家的位置匹配,则设置target_item。然后,如果target_item为空,我们告诉玩家没有东西可以捡起。如果不是,它为我们刚刚添加的系统添加一个拾取请求。

如果你现在运行项目,你可以在任何地方按 g 键,它会告诉你没有什么可以获取。如果你站在一瓶药水上,当你按 g 键时它会消失!它在我们的背包里——但我们除了日志条目外没有任何方法知道这一点。

列出您的库存

能够查看您的库存列表是个好主意!这将是一个游戏模式——也就是说,游戏循环可以进入的另一种状态。因此,首先,我们将在main.rs中扩展RunMode以包含它:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory }
}

i 键是库存的一个流行选择(b 也很流行!),所以在 player.rs 中,我们将在玩家输入代码中添加以下内容:

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

main.rstick 函数中,我们将添加另一个匹配:

#![allow(unused)]
fn main() {
RunState::ShowInventory => {
    if gui::show_inventory(self, ctx) == gui::ItemMenuResult::Cancel {
        newrunstate = RunState::AwaitingInput;
    }
}
}

这自然引出了实现show_inventory!在gui.rs中,我们添加:

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

pub fn show_inventory(gs : &mut State, ctx : &mut Rltk) -> ItemMenuResult {
    let player_entity = gs.ecs.fetch::<Entity>();
    let names = gs.ecs.read_storage::<Name>();
    let backpack = gs.ecs.read_storage::<InBackpack>();

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

    let mut j = 0;
    for (_pack, name) in (&backpack, &names).join().filter(|item| item.0.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());
        y += 1;
        j += 1;
    }

    match ctx.key {
        None => ItemMenuResult::NoResponse,
        Some(key) => {
            match key {
                VirtualKeyCode::Escape => { ItemMenuResult::Cancel }
                _ => ItemMenuResult::NoResponse
            }
        }
    }
}
}

这首先使用 Rust 迭代器的 filter 功能来计算背包中的所有物品。然后绘制一个适当大小的框,并用标题和说明装饰它。接下来,迭代所有匹配的物品并以菜单格式呈现它们。最后,等待键盘输入——如果你按下 ESCAPE,则表示是时候关闭菜单了。

如果你现在 cargo run 你的项目,你可以看到你收集的物品:

截图

使用物品

现在我们可以显示我们的库存了,让我们实际选择一个物品并使用它。我们将扩展菜单以返回一个物品实体和一个结果:

#![allow(unused)]
fn main() {
pub fn show_inventory(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::<InBackpack>();
    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), "Inventory");
    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)
                }
            }
        }
    }
}
}

我们在 main.rs 中调用 show_inventory 现在是无效的,所以我们来修复它:

#![allow(unused)]
fn main() {
RunState::ShowInventory => {
    let result = gui::show_inventory(self, ctx);
    match result.0 {
        gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput,
        gui::ItemMenuResult::NoResponse => {}
        gui::ItemMenuResult::Selected => {
            let item_entity = result.1.unwrap();
            let names = self.ecs.read_storage::<Name>();
            let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>();
            gamelog.entries.push(format!("You try to use {}, but it isn't written yet", names.get(item_entity).unwrap().name));
            newrunstate = RunState::AwaitingInput;
        }
    }
}
}

如果你现在尝试使用库存中的物品,你会得到一条日志记录,表明你尝试使用它,但我们还没有编写那部分代码。这是一个开始!

再次,我们希望编写通用代码——以便最终怪物可以使用药水。我们将暂时作弊,假设所有物品都是药水,并创建一个药水系统;我们稍后会将其变成更有用的东西。因此,我们将首先在 components.rs 中创建一个“意图”组件(并在 main.rs 中注册):

#![allow(unused)]
fn main() {
#[derive(Component, Debug)]
pub struct WantsToDrinkPotion {
    pub potion : Entity
}
}

将以下内容添加到 inventory_system.rs

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

impl<'a> System<'a> for PotionUseSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( ReadExpect<'a, Entity>,
                        WriteExpect<'a, GameLog>,
                        Entities<'a>,
                        WriteStorage<'a, WantsToDrinkPotion>,
                        ReadStorage<'a, Name>,
                        ReadStorage<'a, Potion>,
                        WriteStorage<'a, CombatStats>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (player_entity, mut gamelog, entities, mut wants_drink, names, potions, mut combat_stats) = data;

        for (entity, drink, stats) in (&entities, &wants_drink, &mut combat_stats).join() {
            let potion = potions.get(drink.potion);
            match potion {
                None => {}
                Some(potion) => {
                    stats.hp = i32::min(stats.max_hp, stats.hp + potion.heal_amount);
                    if entity == *player_entity {
                        gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(drink.potion).unwrap().name, potion.heal_amount));
                    }
                    entities.delete(drink.potion).expect("Delete failed");
                }
            }
        }

        wants_drink.clear();
    }
}
}

并在系统列表中注册以运行:

#![allow(unused)]
fn main() {
let mut potions = PotionUseSystem{};
potions.run_now(&self.ecs);
}

像我们看过的其他系统一样,这个迭代所有的WantsToDrinkPotion意图对象。然后根据Potion组件中设置的量治愈饮用者,并删除药水。由于所有的放置信息都附加在药水本身上,因此无需四处检查以确保它从适当的背包中移除:实体不复存在,并带走其组件。

使用 cargo run 测试这个会带来一个惊喜:使用后药水没有被删除!这是因为 ECS 只是将实体标记为 dead - 它在系统中不会删除它们(以免打乱迭代器和线程)。因此,每次调用 dispatch 后,我们需要添加一个对 maintain 的调用。在 main.ecs 中:

#![allow(unused)]
fn main() {
RunState::PreRun => {
    self.run_systems();
    self.ecs.maintain();
    newrunstate = RunState::AwaitingInput;
}
...



RunState::PlayerTurn => {
    self.run_systems();
    self.ecs.maintain();
    newrunstate = RunState::MonsterTurn;
}
RunState::MonsterTurn => {
    self.run_systems();
    self.ecs.maintain();
    newrunstate = RunState::AwaitingInput;
}
}

最后,如果选择了物品,我们需要更改 RunState::ShowInventory 的处理,创建一个 WantsToDrinkPotion 意图:

#![allow(unused)]
fn main() {
RunState::ShowInventory => {
                let result = gui::show_inventory(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::<WantsToDrinkPotion>();
                        intent.insert(*self.ecs.fetch::<Entity>(), WantsToDrinkPotion{ potion: item_entity }).expect("Unable to insert intent");
                        newrunstate = RunState::PlayerTurn;
                    }
                }
            }
}

现在,如果你 cargo run 这个项目,你可以拾取并饮用生命药水:

截图

丢弃物品

你可能希望能够从你的库存中丢弃物品,特别是以后可以用作诱饵的时候。我们将遵循类似的模式来处理这一部分——创建一个意图组件,一个选择它的菜单,以及一个执行丢弃的系统。

所以我们创建一个组件(在 components.rs 中),并在 main.rs 中注册它:

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

我们在 inventory_system.rs 中添加另一个系统:

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

impl<'a> System<'a> for ItemDropSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( ReadExpect<'a, Entity>,
                        WriteExpect<'a, GameLog>,
                        Entities<'a>,
                        WriteStorage<'a, WantsToDropItem>,
                        ReadStorage<'a, Name>,
                        WriteStorage<'a, Position>,
                        WriteStorage<'a, InBackpack>
                      );

    fn run(&mut self, data : Self::SystemData) {
        let (player_entity, mut gamelog, entities, mut wants_drop, names, mut positions, mut backpack) = data;

        for (entity, to_drop) in (&entities, &wants_drop).join() {
            let mut dropper_pos : Position = Position{x:0, y:0};
            {
                let dropped_pos = positions.get(entity).unwrap();
                dropper_pos.x = dropped_pos.x;
                dropper_pos.y = dropped_pos.y;
            }
            positions.insert(to_drop.item, Position{ x : dropper_pos.x, y : dropper_pos.y }).expect("Unable to insert position");
            backpack.remove(to_drop.item);

            if entity == *player_entity {
                gamelog.entries.push(format!("You drop the {}.", names.get(to_drop.item).unwrap().name));
            }
        }

        wants_drop.clear();
    }
}
}

main.rs 中的调度构建器中注册它:

#![allow(unused)]
fn main() {
let mut drop_items = ItemDropSystem{};
drop_items.run_now(&self.ecs);
}

我们将在 main.rs 中添加一个新的 RunState

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum RunState {
    AwaitingInput,
    PreRun,
    PlayerTurn,
    MonsterTurn,
    ShowInventory,
    ShowDropItem
}
}

现在在 player.rs 中,我们将 d 用于 丢弃 添加到命令列表中:

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

gui.rs 中,我们需要另一个菜单——这次是用于丢弃物品的:

#![allow(unused)]
fn main() {
pub fn drop_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::<InBackpack>();
    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), "Drop 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)
                }
            }
        }
    }
}
}

我们也需要扩展main.rs中的状态处理程序以使用它:

#![allow(unused)]
fn main() {
RunState::ShowDropItem => {
    let result = gui::drop_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::<WantsToDropItem>();
            intent.insert(*self.ecs.fetch::<Entity>(), WantsToDropItem{ item: item_entity }).expect("Unable to insert intent");
            newrunstate = RunState::PlayerTurn;
        }
    }
}
}

如果你运行项目,现在可以按 d 来丢弃物品!这里有一个在受到围攻时不太明智地丢弃药水的截图:

截图

渲染顺序

你可能已经注意到,当你走过一瓶药水时,它会覆盖在你上方——完全移除了你玩家的环境!我们将通过在Renderables中添加一个render_order字段来修复这个问题:

#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct Renderable {
    pub glyph: rltk::FontCharType,
    pub fg: RGB,
    pub bg: RGB,
    pub render_order : i32
}
}

您的 IDE 可能现在正在突出显示许多没有这些信息的Renderable组件的错误。我们将在各个地方添加它:玩家是0(首先渲染),怪物是1(第二),物品是2(最后)。例如,在Player生成器中,Renderable现在看起来像这样:

#![allow(unused)]
fn main() {
.with(Renderable {
    glyph: rltk::to_cp437('@'),
    fg: RGB::named(rltk::YELLOW),
    bg: RGB::named(rltk::BLACK),
    render_order: 0
})
}

要使这个某些事情,我们转到main.rs中的项目渲染代码,并对迭代器进行排序。我们参考了规格书来了解如何做到这一点!基本上,我们获取PositionRenderable组件的联合集,并将它们收集到一个向量中。然后我们对向量进行排序,并迭代它以按适当顺序渲染。在main.rs中,用以下代码替换之前的实体渲染代码:

#![allow(unused)]
fn main() {
let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
for (pos, render) 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) }
}
}

总结

本章展示了使用 ECS 的强大功能:拾取、使用和丢弃实体相对简单——一旦玩家可以做到,其他任何东西也可以(如果你将其添加到它们的 AI 中)。我们还展示了如何排序 ECS 获取,以保持合理的渲染顺序。

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

在浏览器中使用 Web Assembly 运行本章示例(需要 WebGL2)


版权 (C) 2019, Herbert Wolverson.

版权 (C) 2024, myedgetech.com.