关于本教程
本教程是免费且开源的,所有代码使用 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};
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()
}
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() {
pub fn spawn_room(ecs: &mut World, room : &Rect) {
let mut monster_spawn_points : Vec<usize> = Vec::new();
{
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;
}
}
}
}
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();
{
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;
}
}
}
}
for idx in monster_spawn_points.iter() {
let x = *idx % MAPWIDTH;
let y = *idx / MAPWIDTH;
random_monster(ecs, x as i32, y as i32);
}
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.