物品和库存
关于本教程
本教程是免费且开源的,所有代码使用 MIT 许可证——因此您可以随意使用它。我希望您会喜欢这个教程,并制作出很棒的游戏!
如果你喜欢这个并且希望我继续写作,请考虑支持我的 Patreon。
到目前为止,我们有地图、怪物和砸东西!没有捡拾物品的“谋杀霍博”体验,就不是完整的 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.rs
的 tick
函数中,我们将添加另一个匹配:
#![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
中的项目渲染代码,并对迭代器进行排序。我们参考了规格书来了解如何做到这一点!基本上,我们获取Position
和Renderable
组件的联合集,并将它们收集到一个向量中。然后我们对向量进行排序,并迭代它以按适当顺序渲染。在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.