效果
关于本教程
本教程是免费和开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
在上一章中,我们为魔法物品添加了物品识别功能 - 并且清楚地表明我们可以创建大量物品。 我们的物品栏系统严重超载 - 它在一个地方做了太多事情,从装备/卸下物品到魔法飞弹法术飞行的内部机制。 更糟糕的是,我们悄无声息地撞到了一堵墙:Specs 限制了您可以传递到系统中的数据存储的数量(并且可能会继续这样做,直到 Rust 支持 C++ 风格的可变参数包)。 我们可以只是绕过这个问题,但通过实施更通用的解决方案来彻底解决这个问题会更好。 它还使我们能够解决一个我们尚未意识到的问题:处理来自物品以外的东西的效果,例如法术(或做古怪事情的陷阱等)。 这也是一个机会来修复您可能没有注意到的错误; 一个实体只能有一个给定类型的组件,因此如果两个东西在一个给定的 tick 中对一个组件造成了伤害 - 实际上只有一次伤害会发生!
什么是效果?
为了正确地建模效果,我们需要思考它们是什么。 效果是某物正在做某事。 它可能是一把剑击中目标,一个法术从深渊召唤出一个强大的恶魔,或者一根魔杖清除召唤出一堆鲜花 - 实际上几乎任何东西! 我们希望保持事物产生多个效果的能力(如果您向物品添加了多个组件,它将触发所有组件 - 这是一件好事;一根雷霆与闪电之杖很容易具有两个或多个效果!)。 因此,由此我们可以推断出:
- 一个效果做一件事情 - 但一个效果的来源可能会产生多个效果。 因此,效果非常适合作为其自身的
Entity
。 - 效果有一个来源:例如,如果效果杀死了某人,则必须有人获得经验值。 它还需要可以选择不有来源 - 它可能纯粹是环境的。
- 效果有一个或多个目标; 它可以是自我目标、针对另一个人或范围效果。 因此,目标是实体或位置。
- 效果可能会触发链中其他效果的产生(例如,想想连锁闪电)。
- 效果做某事,但我们真的不想在早期规划阶段明确具体做什么!
- 我们希望效果来源于多个地方:使用物品、触发陷阱、怪物的特殊攻击、魔法武器的“触发”效果、施法,甚至环境效果!
所以,我们要求的不多! 幸运的是,这完全在我们使用 ECS 可以管理的范围内。 我们将稍微扩展“S”(系统),并使用更通用的工厂模型来实际创建效果 - 然后一旦我们到位,就可以获得相对通用设置的好处。
物品栏系统:快速清理
在我们深入之前,我们应该花一点时间将物品栏系统分解为一个模块。 我们将保留它已经具有的完全相同的功能(目前),但它是一个怪物 - 而怪物通常最好分块处理! 创建一个新文件夹 src/inventory_system
并将 inventory_system.rs
移动到其中 - 并将其重命名为 mod.rs
。 这会将其转换为多文件模块。 (这些步骤实际上足以让您获得可运行的设置 - 这很好地说明了 Rust 中模块的工作原理;名为 inventory_system.rs
的文件是一个模块,inventory_system/mod.rs
也是如此)。
现在打开 inventory_system/mod.rs
,您会看到它包含许多系统:
ItemCollectionSystem
ItemUseSystem
ItemDropSystem
ItemRemoveSystem
ItemIdentificationSystem
我们将为每个系统创建一个新文件,从 mod.rs
中剪切系统代码并将其粘贴到其自己的文件中。 我们需要将 mod.rs
的 use
部分复制到这些文件的顶部,然后删除我们不使用的部分。 最后,我们将在 mod.rs
中添加 mod X
、use X::SystemName
行,以告诉编译器该模块正在共享这些系统。 如果我粘贴这些更改中的每一个,这将是一个巨大的章节,并且由于最大的更改 - ItemUseSystem
将会发生巨大变化,那将是相当大的空间浪费。 相反,我们将浏览第一个 - 您可以查看源代码 以查看其余部分。
例如,我们创建一个新文件 inventory_system/collection_system.rs
:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog::GameLog, EquipmentChanged }; 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>, WriteStorage<'a, EquipmentChanged> ); fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, mut wants_pickup, mut positions, names, mut backpack, mut dirty) = 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"); dirty.insert(pickup.collected_by, EquipmentChanged{}).expect("Unable to insert"); if pickup.collected_by == *player_entity { gamelog.entries.push(format!("You pick up the {}.", names.get(pickup.item).unwrap().name)); } } wants_pickup.clear(); } } }
这完全是原始系统中的代码,这就是我们在此处不重复所有代码的原因。 唯一的区别是我们浏览了顶部的 use super::
列表,并删除了我们不使用的内容。 您可以对 inventory_system/drop_system.rs
、inventory_system/identification_system.rs
、inventory_system/remove_system.rs
和 use_system.rs
执行相同的操作。 然后,将它们捆绑到 inventory_system/mod.rs
中:
#![allow(unused)] fn main() { use super::{WantsToPickupItem, Name, InBackpack, Position, gamelog, WantsToUseItem, Consumable, ProvidesHealing, WantsToDropItem, InflictsDamage, Map, SufferDamage, AreaOfEffect, Confusion, Equippable, Equipped, WantsToRemoveItem, particle_system, ProvidesFood, HungerClock, HungerState, MagicMapper, RunState, Pools, EquipmentChanged, TownPortal, IdentifiedItem, Item, ObfuscatedName}; mod collection_system; pub use collection_system::ItemCollectionSystem; mod use_system; pub use use_system::ItemUseSystem; mod drop_system; pub use drop_system::ItemDropSystem; mod remove_system; pub use remove_system::ItemRemoveSystem; mod identification_system; pub use identification_system::ItemIdentificationSystem; }
我们调整了一些 use
路径以使其他组件感到满意,然后添加了一对 mod
(使用文件)和 pub use
(与项目的其余部分共享)。
如果一切顺利,cargo run
将为您提供与之前完全相同的游戏! 它甚至应该编译得更快一些。
一个新的效果模块
我们将从基础知识开始。 创建一个新文件夹 src/effects
并在其中放置一个名为 mod.rs
的文件。 正如您之前所见,这将创建一个名为 effects 的基本模块。 现在开始有趣的部分; 我们需要能够从任何地方添加效果,包括系统内部:因此无法传入 World
。 但是,生成效果将需要完全的 World
访问权限! 因此,我们将创建一个排队系统。 调用在排队效果,并在稍后扫描队列会导致效果触发。 这基本上是一个消息传递系统,您通常会在大型游戏引擎中找到类似的东西。 因此,这是一个非常简单的 effects/mod.rs
(还要在 main.rs
中的 use
列表中添加 pub mod effects;
,将其包含在您的编译中并使其可供其他模块使用):
#![allow(unused)] fn main() { use std::sync::Mutex; use specs::prelude::*; use std::collections::VecDeque; lazy_static! { pub static ref EFFECT_QUEUE : Mutex<VecDeque<EffectSpawner>> = Mutex::new(VecDeque::new()); } pub enum EffectType { Damage { amount : i32 } } #[derive(Clone)] pub enum Targets { Single { target : Entity }, Area { target: Vec<Entity> } } pub struct EffectSpawner { pub creator : Option<Entity>, pub effect_type : EffectType, pub targets : Targets } pub fn add_effect(creator : Option<Entity>, effect_type: EffectType, targets : Targets) { EFFECT_QUEUE .lock() .unwrap() .push_back(EffectSpawner{ creator, effect_type, targets }); } }
如果您正在使用 IDE,它会抱怨所有这些都没有被使用。 没关系,我们首先构建基本功能! VecDeque
是新的; 它是一个队列(实际上是一个双端队列),其后有一个向量以提高性能。 它允许您添加到任一端,并从中 pop
结果。 请参阅文档 以了解更多信息。
排队伤害
让我们从一个相对简单的开始。 目前,每当实体受到伤害时,我们都会为其分配一个 SufferDamage
组件。 这工作正常,但存在我们之前讨论过的问题 - 一次只能有一个伤害源。 我们希望以多种方式同时谋杀我们的玩家(只是稍微开玩笑一下)! 因此,我们将扩展基础以允许插入伤害。 我们将更改 EffectType
以具有 Damage
类型:
#![allow(unused)] fn main() { pub enum EffectType { Damage { amount : i32 } } }
请注意,我们没有存储受害者或发起者 - 这些都包含在消息的 source 和 target 部分中。 现在我们搜索我们的代码,看看我们在哪里使用 SufferDamage
组件。 最重要的用户是饥饿系统、近战系统、物品使用系统和触发系统:它们都可能导致伤害发生。 打开 melee_combat_system.rs
并找到以下行(在我的源代码中是第 106 行):
#![allow(unused)] fn main() { SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage, from_player: entity == *player_entity); }
我们可以用对队列插入的调用来替换它:
#![allow(unused)] fn main() { add_effect( Some(entity), EffectType::Damage{ amount: damage }, Targets::Single{ target: wants_melee.target } ); }
我们还可以从系统中删除对 inflict_damage
的所有引用,因为我们不再使用它。
我们应该对 trigger_system.rs
做同样的事情。 我们可以替换以下行:
#![allow(unused)] fn main() { SufferDamage::new_damage(&mut inflict_damage, entity, damage.damage, false); }
替换为:
#![allow(unused)] fn main() { add_effect( None, EffectType::Damage{ amount: damage.damage }, Targets::Single{ target: entity } ); }
同样,我们也可以摆脱对 SufferDamage
的所有引用。
我们将暂时忽略 item_use_system
(稍后我们将回到它,我保证)。
应用伤害
因此,现在如果您击中某物,您正在将伤害添加到队列中(并且没有其他事情发生)。 下一步是读取效果队列并对其执行某些操作。 我们将为此采用调度器模型:读取队列,并将命令调度到相关位置。 我们将从骨架开始; 在 effects/mod.rs
中,我们添加以下函数:
#![allow(unused)] fn main() { pub fn run_effects_queue(ecs : &mut World) { loop { let effect : Option<EffectSpawner> = EFFECT_QUEUE.lock().unwrap().pop_front(); if let Some(effect) = effect { // target_applicator(ecs, &effect); // 当我们编写此函数时取消注释! } else { break; } } } }
这非常简单! 它获取锁,时间足够长,可以从队列中弹出第一条消息,如果它有值 - 则对其执行某些操作。 然后,它重复锁/弹出循环,直到队列完全为空。 这是一种有用的模式:锁仅在足够长的时间内持有以读取队列,因此如果内部的任何系统想要添加到队列中,您都不会遇到“死锁”(两个系统永久等待队列访问)。
它还没有对数据做任何事情 - 但这向您展示了如何一次排空队列一条消息。 我们正在接受 World
,因为我们希望修改它。 我们应该添加一个调用来使用此函数; 在 main.rs
中找到 run_systems
并几乎在最后添加它(在粒子和照明之后):
#![allow(unused)] fn main() { effects::run_effects_queue(&mut self.ecs); let mut particles = particle_system::ParticleSpawnSystem{}; particles.run_now(&self.ecs); let mut lighting = lighting_system::LightingSystem{}; lighting.run_now(&self.ecs); }
既然我们正在排空队列,让我们用它做点什么。 在 effects/mod.rs
中,我们将添加注释掉的函数 target_applicator
。 想法是获取 TargetType
,并将其扩展为处理它的调用(该函数具有很高的“扇出” - 意味着我们将经常调用它,并且它将调用许多其他函数)。 有几种不同的方法可以影响目标,因此这里有几个相关函数:
#![allow(unused)] fn main() { fn target_applicator(ecs : &mut World, effect : &EffectSpawner) { match &effect.targets { Targets::Tile{tile_idx} => affect_tile(ecs, effect, *tile_idx), Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), Targets::Single{target} => affect_entity(ecs, effect, *target), Targets::TargetList{targets} => targets.iter().for_each(|entity| affect_entity(ecs, effect, *entity)), } } fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true } } fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx : i32) { if tile_effect_hits_entities(&effect.effect_type) { let content = ecs.fetch::<Map>().tile_content[tile_idx as usize].clone(); content.iter().for_each(|entity| affect_entity(ecs, effect, *entity)); } // TODO: 运行效果 } fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { // TODO: 运行效果 } }
这里有很多需要解释的地方,但它为处理效果目标提供了非常通用的机制。 让我们逐步了解它:
- 调用
target_applicator
。 - 它匹配效果的
targets
字段:- 如果它是
Tile
目标类型,则使用目标瓦片的索引调用Targets::tile
。affect_tile
调用另一个函数tile_effect_hits_entities
,该函数查看请求的效果类型并确定是否应将其应用于瓦片内的实体。 现在,我们只有Damage
- 这对于传递给实体来说是有意义的,因此它当前始终返回 true。- 如果它确实影响了瓦片中的实体,则它从地图中检索瓦片内容 - 并对瓦片中的每个实体调用
affect_entity
。 我们稍后会看到这一点。 - 如果有与瓦片相关的事情要做,它会在这里发生。 现在,它是一个
TODO
注释。
- 如果它是
Tiles
目标类型,它会迭代列表中的所有瓦片,依次对每个瓦片调用affect_tile
- 就像单个瓦片(如上所述),但涵盖了它们中的每一个。 - 如果它是
Single
实体目标,它会为该目标调用affect_entity
。 - 如果它是
TargetList
(目标实体列表),它会依次为这些目标实体中的每一个调用affect_entity
。
- 如果它是
因此,此框架允许我们拥有可以击中瓦片(并可选择击中其中的所有人)、一组瓦片(再次,可选择包括内容)、单个实体或实体列表的效果。 您可以使用它来描述几乎任何目标机制!
接下来,在 run_effects_queue
函数中,取消注释调用者(以便我们的辛勤工作实际运行!):
#![allow(unused)] fn main() { pub fn run_effects_queue(ecs : &mut World) { loop { let effect : Option<EffectSpawner> = EFFECT_QUEUE.lock().unwrap().pop_front(); if let Some(effect) = effect { target_applicator(ecs, &effect); } else { break; } } } }
回到我们正在实现的 Damage
类型,我们需要实现它! 我们将创建一个新文件 effects/damage.rs
并将应用伤害的代码放入其中。 伤害是一次性的、非持久性的东西 - 因此我们将立即处理它。 这是最基本的功能:
#![allow(unused)] fn main() { use specs::prelude::*; use super::*; use crate::components::Pools; pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let EffectType::Damage{amount} = damage.effect_type { pool.hit_points.current -= amount; } } } } }
请注意,我们没有处理血迹、经验值或任何类似的东西! 但是,我们正在应用伤害。 如果您现在 cargo run
,您可以进行近战(并且这样做不会获得任何好处)。
为血神献血!
我们之前的版本在每次造成伤害时都会生成血迹。 将其包含在上面的 inflict_damage
函数中很容易,但我们可能在其他地方使用血迹! 我们还需要验证我们的效果消息队列是否真的足够智能以处理事件期间的插入。 因此,我们将使血迹成为一种效果。 我们将其添加到 effects/mod.rs
中的 EffectType
枚举中:
#![allow(unused)] fn main() { pub enum EffectType { Damage { amount : i32 }, Bloodstain } }
血迹对(现在混乱的)瓦片中的实体没有影响,因此我们将更新 tile_effect_hits_entities
以使其默认不执行任何操作(这样我们就可以不断添加装饰效果,而无需记住每次都添加它):
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, _ => false } } }
同样,affect_entity
可以忽略该事件 - 以及其他装饰事件:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), _ => {} } } }
我们确实希望它影响瓦片,因此我们将更新 affect_tile
以调用血迹函数。
#![allow(unused)] fn main() { fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx : i32) { if tile_effect_hits_entities(&effect.effect_type) { let content = ecs.fetch::<Map>().tile_content[tile_idx as usize].clone(); content.iter().for_each(|entity| affect_entity(ecs, effect, *entity)); } match &effect.effect_type { EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx), _ => {} } } }
现在,在 effects/damage.rs
中,我们将编写血迹代码:
#![allow(unused)] fn main() { pub fn bloodstain(ecs: &mut World, tile_idx : i32) { let mut map = ecs.fetch_mut::<Map>(); map.bloodstains.insert(tile_idx as usize); } }
我们还将更新 inflict_damage
以生成血迹:
#![allow(unused)] fn main() { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let EffectType::Damage{amount} = damage.effect_type { pool.hit_points.current -= amount; if let Some(tile_idx) = entity_position(ecs, target) { add_effect(None, EffectType::Bloodstain, Targets::Tile{tile_idx}); } } } } } }
相关代码向一个神秘函数 entity_position
请求数据 - 如果它返回值,则插入一个 Bloodstain
类型的效果,其中包含瓦片索引。 那么这个函数是什么呢? 我们将定位很多目标,因此我们应该制作一些辅助函数,以便调用者更容易使用该过程。 创建一个新文件 effects/targeting.rs
并将以下内容放入其中:
#![allow(unused)] fn main() { use specs::prelude::*; use crate::components::Position; use crate::map::Map; pub fn entity_position(ecs: &World, target: Entity) -> Option<i32> { if let Some(pos) = ecs.read_storage::<Position>().get(target) { let map = ecs.fetch::<Map>(); return Some(map.xy_idx(pos.x, pos.y) as i32); } None } }
现在在 effects/mods.rs
中添加几行,将目标辅助函数暴露给效果模块的消费者:
#![allow(unused)] fn main() { mod targeting; pub use targeting::*; }
那么这是做什么的呢? 它遵循我们经常使用的模式:它检查实体是否具有位置。 如果是,则从全局地图获取瓦片索引并返回它 - 否则,它返回 None
。
如果您现在 cargo run
,并攻击一个无辜的啮齿动物,您将看到血液! 我们已经证明事件系统不会死锁,并且我们添加了一种添加血迹的简单方法。 您可以从任何地方调用该事件,血液将倾盆而下!
微粒物质
您可能已经注意到,当实体受到伤害时,我们会生成一个粒子。 粒子是我们也可以大量使用的东西,因此将它们作为事件类型也很有意义。 到目前为止,每当我们造成伤害时,我们都会在受害者身上闪烁橙色指示器。 我们不妨在伤害系统中对其进行编纂(并为以后的章节改进留出空间)。 我们很可能也希望出于其他目的启动粒子 - 因此我们将提出另一个相当通用的设置。
我们将从 effects/mod.rs
开始,并将 EffectType
扩展为包含粒子:
#![allow(unused)] fn main() { pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 } } }
您会注意到,我们再次没有指定粒子去哪里; 我们将其留给目标系统。 现在我们将制作一个实际生成粒子的函数。 为了清晰起见,我们将其放在自己的文件中; 在新文件 effects/particles.rs
中添加以下内容:
#![allow(unused)] fn main() { use specs::prelude::*; use super::*; use crate::particle_system::ParticleBuilder; use crate::map::Map; pub fn particle_to_tile(ecs: &mut World, tile_idx : i32, effect: &EffectSpawner) { if let EffectType::Particle{ glyph, fg, bg, lifespan } = effect.effect_type { let map = ecs.fetch::<Map>(); let mut particle_builder = ecs.fetch_mut::<ParticleBuilder>(); particle_builder.request( tile_idx % map.width, tile_idx / map.width, fg, bg, glyph, lifespan ); } } }
这与我们对 ParticleBuilder
的其他调用基本相同,但使用消息的内容来定义要构建的内容。 现在我们将回到 effects/mod.rs
并添加一个 mod particles;
到顶部的使用列表中。 然后我们将扩展 affect_tile
以调用它:
#![allow(unused)] fn main() { fn affect_tile(ecs: &mut World, effect: &EffectSpawner, tile_idx : i32) { if tile_effect_hits_entities(&effect.effect_type) { let content = ecs.fetch::<Map>().tile_content[tile_idx as usize].clone(); content.iter().for_each(|entity| affect_entity(ecs, effect, *entity)); } match &effect.effect_type { EffectType::Bloodstain => damage::bloodstain(ecs, tile_idx), EffectType::Particle{..} => particles::particle_to_tile(ecs, tile_idx, &effect), _ => {} } } }
能够将粒子附加到实体也非常方便,即使它实际上没有太大的效果。 在某些情况下,我们检索了 Position
组件只是为了放置效果,因此这可以让我们简化代码! 因此,我们像这样扩展 affect_entity
:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, _ => {} } } }
因此,现在我们可以打开 effects/damage.rs
,既可以清理血迹代码,又可以应用伤害粒子:
#![allow(unused)] fn main() { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let EffectType::Damage{amount} = damage.effect_type { pool.hit_points.current -= amount; add_effect(None, EffectType::Bloodstain, Targets::Single{target}); add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg : rltk::RGB::named(rltk::ORANGE), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, Targets::Single{target} ); } } } } }
现在打开 melee_combat_system.rs
。 我们可以通过删除伤害时的粒子调用来简化它,并用效果调用替换对 ParticleBuilder
的其他调用。 这使我们可以摆脱对粒子系统、位置和玩家实体的所有引用! 这是我想要的改进:系统正在简化为它们应该关注的内容! 有关更改,请参阅源代码; 它们太长了,无法在此处正文文本中包含。
如果您现在 cargo run
,如果您伤害了某些东西,您将看到粒子 - 血迹应该仍然有效。
经验值
因此,我们仍然缺少一些重要的东西:当您杀死怪物时,它应该掉落战利品/现金,提供经验值等等。 与其用太多无关的东西污染“伤害”函数(基于函数做好一件事的原则),不如添加一个新的 EffectType
- EntityDeath
:
#![allow(unused)] fn main() { pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath } }
现在在 inflict_damage
中,如果实体死亡,我们将发出此事件:
#![allow(unused)] fn main() { if pool.hit_points.current < 1 { add_effect(damage.creator, EffectType::EntityDeath, Targets::Single{target}); } }
我们还将创建一个新函数; 这与 damage_system
中的代码相同(当我们处理完物品使用后,我们将删除系统的大部分内容):
#![allow(unused)] fn main() { pub fn death(ecs: &mut World, effect: &EffectSpawner, target : Entity) { let mut xp_gain = 0; let mut gold_gain = 0.0f32; let mut pools = ecs.write_storage::<Pools>(); let attributes = ecs.read_storage::<Attributes>(); let mut map = ecs.fetch_mut::<Map>(); if let Some(pos) = entity_position(ecs, target) { crate::spatial::remove_entity(target, pos as usize); } if let Some(source) = effect.creator { if ecs.read_storage::<Player>().get(source).is_some() { if let Some(stats) = pools.get(target) { xp_gain += stats.level * 100; gold_gain += stats.gold; } if xp_gain != 0 || gold_gain != 0.0 { let mut log = ecs.fetch_mut::<GameLog>(); let mut player_stats = pools.get_mut(source).unwrap(); let player_attributes = attributes.get(source).unwrap(); player_stats.xp += xp_gain; player_stats.gold += gold_gain; if player_stats.xp >= player_stats.level * 1000 { // 我们升级了! player_stats.level += 1; log.entries.push(format!("Congratulations, you are now level {}", player_stats.level)); player_stats.hit_points.max = player_hp_at_level( player_attributes.fitness.base + player_attributes.fitness.modifiers, player_stats.level ); player_stats.hit_points.current = player_stats.hit_points.max; player_stats.mana.max = mana_at_level( player_attributes.intelligence.base + player_attributes.intelligence.modifiers, player_stats.level ); player_stats.mana.current = player_stats.mana.max; let player_pos = ecs.fetch::<rltk::Point>(); for i in 0..10 { if player_pos.y - i > 1 { add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('░'), fg : rltk::RGB::named(rltk::GOLD), bg : rltk::RGB::named(rltk::BLACK), lifespan: 400.0 }, Targets::Tile{ tile_idx : map.xy_idx(player_pos.x, player_pos.y - i) as i32 } ); } } } } } } } }
最后,我们将效果添加到 affect_entity
:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, _ => {} } } }
因此,现在如果您 cargo run
该项目,我们将回到我们原来的位置 - 但具有更灵活的系统来处理粒子、伤害(现在可以堆叠!)和一般杀死事物。
物品效果
现在我们有了效果系统的基础知识(并清理了伤害),是时候真正思考物品(和触发器)应该如何工作了。 我们希望它们足够通用,以便您可以像乐高积木一样组合实体并构建有趣的东西。 我们还希望停止在多个地方定义效果; 目前,我们在一个系统中列出触发效果,在另一个系统中列出物品效果 - 如果我们添加法术,我们将有另一个地方需要调试!
我们将首先查看物品使用系统 (inventory_system/use_system.rs
)。 它非常庞大,并且在一个地方做了太多事情。 它处理目标选择、识别、装备切换、触发使用物品的效果以及消耗品的销毁! 这对于构建一个玩具游戏进行测试来说很好,但它无法扩展到“真实”游戏。
对于其中的一部分 - 并本着使用 ECS 的精神 - 我们将制作更多系统,并让它们做好一件事。
移动装备
装备(和交换)物品目前位于物品使用系统中,因为它从用户界面的角度来看适合在那里:您“使用”一把剑,使用它的逻辑方法是握住它(并收起您手中持有的任何东西)。 虽然将其作为物品使用系统的一部分使系统过于混乱 - 该系统只是做得太多(并且目标选择实际上不是问题,因为您是在自己身上使用它)。
因此,我们将在文件 inventory_system/use_equip.rs
中创建一个新系统,并将功能移动到其中。 这将导致一个紧凑的新系统:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Name, InBackpack, gamelog::GameLog, WantsToUseItem, Equippable, Equipped, EquipmentChanged}; pub struct ItemEquipOnUse {} impl<'a> System<'a> for ItemEquipOnUse { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, ReadStorage<'a, Equippable>, WriteStorage<'a, Equipped>, WriteStorage<'a, InBackpack>, WriteStorage<'a, EquipmentChanged> ); #[allow(clippy::cognitive_complexity)] fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, entities, mut wants_use, names, equippable, mut equipped, mut backpack, mut dirty) = data; let mut remove_use : Vec<Entity> = Vec::new(); for (target, useitem) in (&entities, &wants_use).join() { // 如果它是可装备的,那么我们想要装备它 - 并卸下该槽位中的任何其他物品 if let Some(can_equip) = equippable.get(useitem.item) { let target_slot = can_equip.slot; // 删除目标在该物品槽位中拥有的任何物品 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)); } // 完成物品 remove_use.push(target); } } remove_use.iter().for_each(|e| { dirty.insert(*e, EquipmentChanged{}).expect("Unable to insert"); wants_use.remove(*e).expect("Unable to remove"); }); } } }
现在进入 use_system.rs
并删除相同的代码块。 最后,跳到 main.rs
并将系统添加到 run_systems
中(就在当前的 use 系统调用之前):
#![allow(unused)] fn main() { let mut itemequip = inventory_system::ItemEquipOnUse{}; itemequip.run_now(&self.ecs); ... let mut itemuse = ItemUseSystem{}; }
继续 cargo run
并切换一些装备,以确保它仍然有效。 这是不错的进展 - 我们可以从我们的 use_system
中删除三个完整的组件存储!
物品效果
现在我们已经将物品栏管理清理到它自己的系统中,是时候真正切入此更改的核心了:具有效果的物品使用。 目标是拥有一个了解物品的系统,但可以“扇出”到通用代码,我们可以将该代码重用于每个其他效果使用。 我们将从 effects/mod.rs
开始,添加一个用于“我想使用物品”的效果类型:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 } } }
我们希望这些效果与常规效果略有不同(必须处理消耗品使用,并且目标选择通过到实际效果,而不是直接从物品传递)。 我们将其添加到 target_applicator
中:
#![allow(unused)] fn main() { fn target_applicator(ecs : &mut World, effect : &EffectSpawner) { if let EffectType::ItemUse{item} = effect.effect_type { triggers::item_trigger(effect.creator, item, &effect.targets, ecs); } else { match &effect.targets { Targets::Tile{tile_idx} => affect_tile(ecs, effect, *tile_idx), Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), Targets::Single{target} => affect_entity(ecs, effect, *target), Targets::TargetList{targets} => targets.iter().for_each(|entity| affect_entity(ecs, effect, *entity)), } } } }
这“短路”了调用树,因此它处理物品一次(物品然后可以将其他事件发射到队列中,因此所有事件都得到处理)。 由于我们已经调用了它,现在我们必须编写 triggers:item_trigger
! 创建一个新文件 effects/triggers.rs
(并在 mod.rs
中添加 mod triggers;
):
#![allow(unused)] fn main() { pub fn item_trigger(creator : Option<Entity>, item: Entity, targets : &Targets, ecs: &mut World) { // 通过通用系统使用物品 event_trigger(creator, item, targets, ecs); // 如果它是消耗品,则将其删除 if ecs.read_storage::<Consumable>().get(item).is_some() { ecs.entities().delete(item).expect("Delete Failed"); } } }
此函数是我们必须以不同方式处理物品的原因:它调用 event_trigger
(一个本地的私有函数)来生成物品的所有效果 - 然后,如果该物品是消耗品,它会将其删除。 让我们创建一个骨架 event_trigger
函数:
#![allow(unused)] fn main() { fn event_trigger(creator : Option<Entity>, entity: Entity, targets : &Targets, ecs: &mut World) { let mut gamelog = ecs.fetch_mut::<GameLog>(); } }
因此,这不会做任何事情 - 但游戏现在可以编译,您可以看到当您使用物品时,它会被正确删除。 它提供了足够的占位符,使我们能够修复物品栏系统!
使用系统清理
inventory_system/use_system.rs
文件是此清理的根本原因,我们现在有足够的框架使其成为一个相当小的精简系统! 我们只需要将其装备标记为已更改,构建适当的 Targets
列表,并添加一个使用事件。 这是整个新系统:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Name, WantsToUseItem,Map, AreaOfEffect, EquipmentChanged, IdentifiedItem}; use crate::effects::*; pub struct ItemUseSystem {} impl<'a> System<'a> for ItemUseSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, Map>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, ReadStorage<'a, AreaOfEffect>, WriteStorage<'a, EquipmentChanged>, WriteStorage<'a, IdentifiedItem> ); #[allow(clippy::cognitive_complexity)] fn run(&mut self, data : Self::SystemData) { let (player_entity, map, entities, mut wants_use, names, aoe, mut dirty, mut identified_item) = data; for (entity, useitem) in (&entities, &wants_use).join() { dirty.insert(entity, EquipmentChanged{}).expect("Unable to insert"); // 识别 if entity == *player_entity { identified_item.insert(entity, IdentifiedItem{ name: names.get(useitem.item).unwrap().name.clone() }) .expect("Unable to insert"); } // 调用效果系统 add_effect( Some(entity), EffectType::ItemUse{ item : useitem.item }, match useitem.target { None => Targets::Single{ target: *player_entity }, Some(target) => { if let Some(aoe) = aoe.get(useitem.item) { Targets::Tiles{ tiles: aoe_tiles(&*map, target, aoe.radius) } } else { Targets::Tile{ tile_idx : map.xy_idx(target.x, target.y) as i32 } } } } ); } wants_use.clear(); } } }
这是一个很大的改进! 小得多,而且非常容易理解。
现在我们需要完成各种与物品相关的事件,并使其发挥作用。
喂食时间
我们将从食物开始。 任何带有 ProvidesFood
组件标签的物品都会将进食者的饥饿时钟设置回 Well Fed
。 我们将首先为此添加一个事件类型:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, } }
现在,我们将创建一个新文件 - effects/hunger.rs
并将处理此问题的核心内容放入其中(不要忘记在 effects/mod.rs
中添加 mod hunger;
!):
#![allow(unused)] fn main() { use specs::prelude::*; use super::*; use crate::components::{HungerClock, HungerState}; pub fn well_fed(ecs: &mut World, _damage: &EffectSpawner, target: Entity) { if let Some(hc) = ecs.write_storage::<HungerClock>().get_mut(target) { hc.state = HungerState::WellFed; hc.duration = 20; } } }
非常简单,并且直接来自原始代码。 我们需要食物影响实体,而不仅仅是位置(以防您制作类似自动售货机的东西,在某个区域分发食物!):
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, EffectType::WellFed => true, _ => false } } }
我们还需要调用该函数:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), _ => {} } } }
最后,我们需要将其添加到 effects/triggers.rs
中的 event_trigger
函数中:
#![allow(unused)] fn main() { fn event_trigger(creator : Option<Entity>, entity: Entity, targets : &Targets, ecs: &mut World) { let mut gamelog = ecs.fetch_mut::<GameLog>(); // 提供食物 if ecs.read_storage::<ProvidesFood>().get(entity).is_some() { add_effect(creator, EffectType::WellFed, targets.clone()); let names = ecs.read_storage::<Name>(); gamelog.entries.push(format!("You eat the {}.", names.get(entity).unwrap().name)); } } }
如果您现在 cargo run
,您可以吃掉您的口粮并再次吃饱。
魔法地图
魔法地图有点特殊,因为需要切换回用户界面进行更新。 它也很简单,因此我们将在 event_trigger
中完全处理它:
#![allow(unused)] fn main() { // 魔法地图 if ecs.read_storage::<MagicMapper>().get(entity).is_some() { let mut runstate = ecs.fetch_mut::<RunState>(); gamelog.entries.push("The map is revealed to you!".to_string()); *runstate = RunState::MagicMapReveal{ row : 0}; } }
就像旧的物品使用系统中的代码一样:它将运行状态设置为 MagicMapReveal
并播放日志消息。 您可以 cargo run
,魔法地图现在可以工作了。
城镇传送门
城镇传送门也有点特殊,因此我们也将在 event_trigger
中处理它们:
#![allow(unused)] fn main() { // 城镇传送门 if ecs.read_storage::<TownPortal>().get(entity).is_some() { let map = ecs.fetch::<Map>(); if map.depth == 1 { gamelog.entries.push("You are already in town, so the scroll does nothing.".to_string()); } else { gamelog.entries.push("You are telported back to town!".to_string()); let mut runstate = ecs.fetch_mut::<RunState>(); *runstate = RunState::TownPortal; } } }
再一次,这基本上是旧代码 - 已重新定位。
治疗
治疗是一种更通用的效果,我们很可能会在多个地方使用它。 很容易想象到一个带有入口触发器的道具可以治愈您(魔法恢复区、电子修复店 - 您的想象力是无限的!),或者使用时可以治愈的物品(例如药水)。 因此,我们将 Healing
添加到 mod.rs
中的效果类型中:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 } } }
治疗影响实体而不是瓦片,因此我们将标记这一点:
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, EffectType::WellFed => true, EffectType::Healing{..} => true, _ => false } } }
由于治疗基本上是反向伤害,我们将在 effects/damage.rs
文件中添加一个函数来处理治疗:
#![allow(unused)] fn main() { pub fn heal_damage(ecs: &mut World, heal: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); if let Some(pool) = pools.get_mut(target) { if let EffectType::Healing{amount} = heal.effect_type { pool.hit_points.current = i32::min(pool.hit_points.max, pool.hit_points.current + amount); add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg : rltk::RGB::named(rltk::GREEN), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, Targets::Single{target} ); } } } }
这与旧的治疗代码类似,但我们添加了一个绿色粒子来显示实体已获得治疗。 现在我们需要教 mod.rs
中的 affect_entity
应用治疗:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), _ => {} } } }
最后,我们在 event_trigger
函数中添加对 ProvidesHealing
标签的支持:
#![allow(unused)] fn main() { // 治疗 if let Some(heal) = ecs.read_storage::<ProvidesHealing>().get(entity) { add_effect(creator, EffectType::Healing{amount: heal.heal_amount}, targets.clone()); } }
如果您现在 cargo run
,您的治疗药水现在可以工作了。
伤害
我们已经编写了处理伤害所需的大部分内容,因此我们可以将其添加到 event_trigger
中:
#![allow(unused)] fn main() { // 伤害 if let Some(damage) = ecs.read_storage::<InflictsDamage>().get(entity) { add_effect(creator, EffectType::Damage{ amount: damage.damage }, targets.clone()); } }
由于我们已经通过目标选择涵盖了范围效果和类似效果,并且伤害代码来自近战改造 - 这将使魔法飞弹、火球和类似效果起作用。
混乱
混乱需要以类似于饥饿的方式处理。 我们添加一个事件类型:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 } } }
将其标记为影响实体:
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, EffectType::WellFed => true, EffectType::Healing{..} => true, EffectType::Confusion{..} => true, _ => false } } }
在 damage.rs
文件中添加一个方法:
#![allow(unused)] fn main() { pub fn add_confusion(ecs: &mut World, effect: &EffectSpawner, target: Entity) { if let EffectType::Confusion{turns} = &effect.effect_type { ecs.write_storage::<Confusion>().insert(target, Confusion{ turns: *turns }).expect("Unable to insert status"); } } }
将其包含在 affect_entity
中:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), EffectType::Confusion{..} => damage::add_confusion(ecs, effect, target), _ => {} } } }
最后,在 event_trigger
中支持它:
#![allow(unused)] fn main() { // 混乱 if let Some(confusion) = ecs.read_storage::<Confusion>().get(entity) { add_effect(creator, EffectType::Confusion{ turns : confusion.turns }, targets.clone()); } }
这就足以使混乱效果起作用。
触发器
既然我们已经有了一个适用于物品的工作系统(它非常灵活;您可以根据需要混合和匹配标签,并且所有效果都会触发),我们需要对触发器执行相同的操作。 我们将首先为它们提供一个进入效果 API 的入口点,就像我们对物品所做的那样。 在 effects/mod.rs
中,我们将进一步扩展物品效果枚举:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 }, TriggerFire { trigger: Entity } } }
我们还将特殊处理它的激活:
#![allow(unused)] fn main() { fn target_applicator(ecs : &mut World, effect : &EffectSpawner) { if let EffectType::ItemUse{item} = effect.effect_type { triggers::item_trigger(effect.creator, item, &effect.targets, ecs); } else if let EffectType::TriggerFire{trigger} = effect.effect_type { triggers::trigger(effect.creator, trigger, &effect.targets, ecs); } else { match &effect.targets { Targets::Tile{tile_idx} => affect_tile(ecs, effect, *tile_idx), Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), Targets::Single{target} => affect_entity(ecs, effect, *target), Targets::TargetList{targets} => targets.iter().for_each(|entity| affect_entity(ecs, effect, *entity)), } } } }
现在在 effects/triggers.rs
中,我们需要添加 trigger
作为公共函数:
#![allow(unused)] fn main() { pub fn trigger(creator : Option<Entity>, trigger: Entity, targets : &Targets, ecs: &mut World) { // 触发物品不再隐藏 ecs.write_storage::<Hidden>().remove(trigger); // 通过通用系统使用物品 event_trigger(creator, trigger, targets, ecs); // 如果它是单次激活,则将其删除 if ecs.read_storage::<SingleActivation>().get(trigger).is_some() { ecs.entities().delete(trigger).expect("Delete Failed"); } } }
现在我们有了一个框架,我们可以进入 trigger_system.rs
。 就像物品效果一样,它可以大大简化; 我们真的只需要检查是否发生了激活 - 并调用事件系统:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{EntityMoved, Position, EntryTrigger, Map, Name, gamelog::GameLog, effects::*, AreaOfEffect}; pub struct TriggerSystem {} impl<'a> System<'a> for TriggerSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Map>, WriteStorage<'a, EntityMoved>, ReadStorage<'a, Position>, ReadStorage<'a, EntryTrigger>, ReadStorage<'a, Name>, Entities<'a>, WriteExpect<'a, GameLog>, ReadStorage<'a, AreaOfEffect>); fn run(&mut self, data : Self::SystemData) { let (map, mut entity_moved, position, entry_trigger, names, entities, mut log, area_of_effect) = data; // 迭代移动的实体及其最终位置 for (entity, mut _entity_moved, pos) in (&entities, &mut entity_moved, &position).join() { let idx = map.xy_idx(pos.x, pos.y); for entity_id in map.tile_content[idx].iter() { if entity != *entity_id { // 不要费心检查自己是否是陷阱! let maybe_trigger = entry_trigger.get(*entity_id); match maybe_trigger { None => {}, Some(_trigger) => { // 我们触发了它 let name = names.get(*entity_id); if let Some(name) = name { log.entries.push(format!("{} triggers!", &name.name)); } // 调用效果系统 add_effect( Some(entity), EffectType::TriggerFire{ trigger : *entity_id }, if let Some(aoe) = area_of_effect.get(*entity_id) { Targets::Tiles{ tiles : aoe_tiles(&*map, rltk::Point::new(pos.x, pos.y), aoe.radius) } } else { Targets::Tile{ tile_idx: idx as i32 } } ); } } } } } // 删除所有实体移动标记 entity_moved.clear(); } } }
我们只有一个触发器尚未作为效果实现:传送。 让我们在 effects/mod.rs
中将其添加为效果类型:
#![allow(unused)] fn main() { #[derive(Debug)] pub enum EffectType { Damage { amount : i32 }, Bloodstain, Particle { glyph: rltk::FontCharType, fg : rltk::RGB, bg: rltk::RGB, lifespan: f32 }, EntityDeath, ItemUse { item: Entity }, WellFed, Healing { amount : i32 }, Confusion { turns : i32 }, TriggerFire { trigger: Entity }, TeleportTo { x:i32, y:i32, depth: i32, player_only : bool } } }
它影响实体,因此我们将标记这一事实:
#![allow(unused)] fn main() { fn tile_effect_hits_entities(effect: &EffectType) -> bool { match effect { EffectType::Damage{..} => true, EffectType::WellFed => true, EffectType::Healing{..} => true, EffectType::Confusion{..} => true, EffectType::TeleportTo{..} => true, _ => false } } }
affect_entity
应该调用它:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &EffectSpawner, target: Entity) { match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), EffectType::Confusion{..} => damage::add_confusion(ecs, effect, target), EffectType::TeleportTo{..} => movement::apply_teleport(ecs, effect, target), _ => {} } } }
我们还需要将其添加到 effects/triggers.rs
中的 event_trigger
:
#![allow(unused)] fn main() { // 传送 if let Some(teleport) = ecs.read_storage::<TeleportTo>().get(entity) { add_effect( creator, EffectType::TeleportTo{ x : teleport.x, y : teleport.y, depth: teleport.depth, player_only: teleport.player_only }, targets.clone() ); } }
最后,我们将实现它。 创建一个新文件 effects/movement.rs
并将以下内容粘贴到其中:
#![allow(unused)] fn main() { use specs::prelude::*; use super::*; use crate::components::{ApplyTeleport}; pub fn apply_teleport(ecs: &mut World, destination: &EffectSpawner, target: Entity) { let player_entity = ecs.fetch::<Entity>(); if let EffectType::TeleportTo{x, y, depth, player_only} = &destination.effect_type { if !player_only || target == *player_entity { let mut apply_teleport = ecs.write_storage::<ApplyTeleport>(); apply_teleport.insert(target, ApplyTeleport{ dest_x : *x, dest_y : *y, dest_depth : *depth }).expect("Unable to insert"); } } } }
现在 cargo run
该项目,然后继续尝试一些触发器。 城镇传送门和陷阱是明显的例子。 您应该能够像以前一样使用传送门并遭受陷阱伤害。
将单次使用限制为在它做了某事时
您可能已经注意到,我们正在拿走您的城镇传送门卷轴,即使它没有激活。 我们正在拿走传送器,即使它实际上没有发射(因为它仅限玩家使用)。 这需要修复! 我们将修改 event_trigger
以返回 bool
- true
如果它做了某些事情,false
如果它没有。 这是执行此操作的版本:
#![allow(unused)] fn main() { fn event_trigger(creator : Option<Entity>, entity: Entity, targets : &Targets, ecs: &mut World) -> bool { let mut did_something = false; let mut gamelog = ecs.fetch_mut::<GameLog>(); // 提供食物 if ecs.read_storage::<ProvidesFood>().get(entity).is_some() { add_effect(creator, EffectType::WellFed, targets.clone()); let names = ecs.read_storage::<Name>(); gamelog.entries.push(format!("You eat the {}.", names.get(entity).unwrap().name)); did_something = true; } // 魔法地图 if ecs.read_storage::<MagicMapper>().get(entity).is_some() { let mut runstate = ecs.fetch_mut::<RunState>(); gamelog.entries.push("The map is revealed to you!".to_string()); *runstate = RunState::MagicMapReveal{ row : 0}; did_something = true; } // 城镇传送门 if ecs.read_storage::<TownPortal>().get(entity).is_some() { let map = ecs.fetch::<Map>(); if map.depth == 1 { gamelog.entries.push("You are already in town, so the scroll does nothing.".to_string()); } else { gamelog.entries.push("You are telported back to town!".to_string()); let mut runstate = ecs.fetch_mut::<RunState>(); *runstate = RunState::TownPortal; did_something = true; } } // 治疗 if let Some(heal) = ecs.read_storage::<ProvidesHealing>().get(entity) { add_effect(creator, EffectType::Healing{amount: heal.heal_amount}, targets.clone()); did_something = true; } // 伤害 if let Some(damage) = ecs.read_storage::<InflictsDamage>().get(entity) { add_effect(creator, EffectType::Damage{ amount: damage.damage }, targets.clone()); did_something = true; } // 混乱 if let Some(confusion) = ecs.read_storage::<Confusion>().get(entity) { add_effect(creator, EffectType::Confusion{ turns : confusion.turns }, targets.clone()); did_something = true; } // 传送 if let Some(teleport) = ecs.read_storage::<TeleportTo>().get(entity) { add_effect( creator, EffectType::TeleportTo{ x : teleport.x, y : teleport.y, depth: teleport.depth, player_only: teleport.player_only }, targets.clone() ); did_something = true; } did_something } }
现在我们需要修改我们的入口点,以便仅删除实际使用的物品:
#![allow(unused)] fn main() { pub fn item_trigger(creator : Option<Entity>, item: Entity, targets : &Targets, ecs: &mut World) { // 通过通用系统使用物品 let did_something = event_trigger(creator, item, targets, ecs); // 如果它是消耗品,则将其删除 if did_something && ecs.read_storage::<Consumable>().get(item).is_some() { ecs.entities().delete(item).expect("Delete Failed"); } } pub fn trigger(creator : Option<Entity>, trigger: Entity, targets : &Targets, ecs: &mut World) { // 触发物品不再隐藏 ecs.write_storage::<Hidden>().remove(trigger); // 通过通用系统使用物品 let did_something = event_trigger(creator, trigger, targets, ecs); // 如果它是单次激活,则将其删除 if did_something && ecs.read_storage::<SingleActivation>().get(trigger).is_some() { ecs.entities().delete(trigger).expect("Delete Failed"); } } }
清理
现在我们已经有了这个系统,我们可以清理所有其他类型的系统。 我们首先可以做的是从 components.rs
中删除 SufferDamage
组件(并从 main.rs
和 saveload_system.rs
中删除它)。 删除此项会导致编译器找到一些我们在没有使用效果系统的情况下造成伤害的地方!
在 hunger_system.rs
中,我们可以用以下内容替换 SufferDamage
代码:
#![allow(unused)] fn main() { HungerState::Starving => { // 饥饿造成的伤害 if entity == *player_entity { log.entries.push("Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string()); } add_effect( None, EffectType::Damage{ amount: 1}, Targets::Single{ target: entity } ); } }
我们还可以打开 damage_system.rs
并删除实际的 DamageSystem
(但保留 delete_the_dead
)。 我们还需要从 main.rs
中的 run_systems
中删除它。
通用生成代码
在 raws/rawmaster.rs
中,我们仍然重复解析物品的可能效果。 不幸的是,传递 EntityBuilder
对象 (eb
) 会导致一些生命周期问题,这些问题导致 Rust 编译器拒绝看起来完全有效的代码。 因此,我们将使用宏来解决这个问题。 在 spawn_named_item
之前:
#![allow(unused)] fn main() { macro_rules! apply_effects { ( $effects:expr, $eb:expr ) => { for effect in $effects.iter() { let effect_name = effect.0.as_str(); match effect_name { "provides_healing" => $eb = $eb.with(ProvidesHealing{ heal_amount: effect.1.parse::<i32>().unwrap() }), "ranged" => $eb = $eb.with(Ranged{ range: effect.1.parse::<i32>().unwrap() }), "damage" => $eb = $eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }), "area_of_effect" => $eb = $eb.with(AreaOfEffect{ radius: effect.1.parse::<i32>().unwrap() }), "confusion" => $eb = $eb.with(Confusion{ turns: effect.1.parse::<i32>().unwrap() }), "magic_mapping" => $eb = $eb.with(MagicMapper{}), "town_portal" => $eb = $eb.with(TownPortal{}), "food" => $eb = $eb.with(ProvidesFood{}), "single_activation" => $eb = $eb.with(SingleActivation{}), _ => rltk::console::log(format!("Warning: consumable effect {} not implemented.", effect_name)) } } }; } }
因此,这就像一个函数,但它遵循相当复杂的宏语法。 基本上,我们将宏定义为期望 effects
和 eb
作为表达式 - 也就是说,我们并不真正在意它们是什么,我们将执行文本替换(在编译之前)以将它们插入到发出的代码中。 (宏基本上是在调用站点复制/粘贴到您的代码中的,但表达式被替换了)。 深入研究 spawn_named_item
,您会看到在消耗品部分,我们正在使用此代码。 我们现在可以用以下内容替换它:
#![allow(unused)] fn main() { if let Some(consumable) = &item_template.consumable { eb = eb.with(crate::components::Consumable{}); apply_effects!(consumable.effects, eb); } }
如果我们转到 spawn_named_prop
,您会看到我们正在做基本相同的事情:
#![allow(unused)] fn main() { for effect in entry_trigger.effects.iter() { match effect.0.as_str() { "damage" => { eb = eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }) } "single_activation" => { eb = eb.with(SingleActivation{}) } _ => {} } } }
我们现在可以用另一个对宏的调用来替换它:
#![allow(unused)] fn main() { if let Some(entry_trigger) = &prop_template.entry_trigger { eb = eb.with(EntryTrigger{}); apply_effects!(entry_trigger.effects, eb); } }
毫无疑问,我们稍后会添加更多 - 用于武器“触发”、法术发射以及使用后不会消耗的物品。 进行此更改意味着相同的定义 JSON 适用于入口触发器和消耗品效果 - 因此任何可以与其中一个效果一起使用的效果都可以与另一个效果一起使用。
这种方法如何提供帮助的一些示例
让我们在神庙中添加一个新的道具:一个可以治愈您的祭坛。 打开 map_builders/town.rs
并找到 build_temple
函数。 将 Altar
添加到道具列表中:
#![allow(unused)] fn main() { fn build_temple(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 let mut to_place : Vec<&str> = vec!["Priest", "Altar", "Parishioner", "Parishioner", "Chair", "Chair", "Candle", "Candle"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } }
现在在 spawns.json
中,我们将 Altar
添加到道具列表中:
{
"name" : "Altar",
"renderable": {
"glyph" : "╫",
"fg" : "#55FF55",
"bg" : "#000000",
"order" : 2
},
"hidden" : false,
"entry_trigger" : {
"effects" : {
"provides_healing" : "100"
}
}
},
您现在可以 cargo run
该项目,损失一些生命值,然后去神庙免费治疗。 我们在没有额外代码的情况下实现了它,因为我们正在共享来自其他物品的效果属性。 从现在开始,当我们添加效果时 - 我们可以随时随地轻松地实现它们。
恢复魔法飞弹和火球的视觉效果
我们重构的一个副作用是,当您施放火球时,您不再获得火焰效果(只有伤害指示器)。 当您用魔法飞弹射击时,您也不会获得漂亮的线条,或者当您使某人混乱时也不会获得标记。 这是故意的 - 之前的范围效果代码显示了任何 AoE 攻击的火球效果! 我们可以通过支持效果作为物品定义的一部分来制作更灵活的系统。
让我们首先用我们希望它们做的事情来装饰 spawns.json
中的两个卷轴:
{
"name" : "Magic Missile Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"particle_line" : "*;#00FFFF;200.0"
}
},
"weight_lbs" : 0.5,
"base_value" : 50.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "scroll" }
},
{
"name" : "Fireball Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#FFA500",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"area_of_effect" : "3",
"particle" : "*;#FFA500;200.0"
}
},
"weight_lbs" : 0.5,
"base_value" : 100.0,
"vendor_category" : "alchemy",
"magic" : { "class" : "common", "naming" : "scroll" }
},
我们添加了两个新条目 - particle
和 particle_line
。 它们都采用相当神秘的字符串(因为我们正在以字符串形式传递参数)。 这是一个分号分隔的列表。 第一个参数是字形,第二个参数是 RGB 格式的颜色,最后一个参数是生命周期。
现在我们需要几个新组件(在 components.rs
中,并在 main.rs
和 saveload_system.rs
中注册)来存储此信息:
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct SpawnParticleLine { pub glyph : rltk::FontCharType, pub color : RGB, pub lifetime_ms : f32 } #[derive(Component, Serialize, Deserialize, Clone)] pub struct SpawnParticleBurst { pub glyph : rltk::FontCharType, pub color : RGB, pub lifetime_ms : f32 } }
现在在 raws/rawmaster.rs
中,我们需要将其解析为效果并附加新组件:
#![allow(unused)] fn main() { fn parse_particle_line(n : &str) -> SpawnParticleLine { let tokens : Vec<_> = n.split(';').collect(); SpawnParticleLine{ glyph : rltk::to_cp437(tokens[0].chars().next().unwrap()), color : rltk::RGB::from_hex(tokens[1]).expect("Bad RGB"), lifetime_ms : tokens[2].parse::<f32>().unwrap() } } fn parse_particle(n : &str) -> SpawnParticleBurst { let tokens : Vec<_> = n.split(';').collect(); SpawnParticleBurst{ glyph : rltk::to_cp437(tokens[0].chars().next().unwrap()), color : rltk::RGB::from_hex(tokens[1]).expect("Bad RGB"), lifetime_ms : tokens[2].parse::<f32>().unwrap() } } macro_rules! apply_effects { ( $effects:expr, $eb:expr ) => { for effect in $effects.iter() { let effect_name = effect.0.as_str(); match effect_name { "provides_healing" => $eb = $eb.with(ProvidesHealing{ heal_amount: effect.1.parse::<i32>().unwrap() }), "ranged" => $eb = $eb.with(Ranged{ range: effect.1.parse::<i32>().unwrap() }), "damage" => $eb = $eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }), "area_of_effect" => $eb = $eb.with(AreaOfEffect{ radius: effect.1.parse::<i32>().unwrap() }), "confusion" => $eb = $eb.with(Confusion{ turns: effect.1.parse::<i32>().unwrap() }), "magic_mapping" => $eb = $eb.with(MagicMapper{}), "town_portal" => $eb = $eb.with(TownPortal{}), "food" => $eb = $eb.with(ProvidesFood{}), "single_activation" => $eb = $eb.with(SingleActivation{}), "particle_line" => $eb = $eb.with(parse_particle_line(&effect.1)), "particle" => $eb = $eb.with(parse_particle(&effect.1)), _ => rltk::console::log(format!("Warning: consumable effect {} not implemented.", effect_name)) } } }; } }
实现粒子爆发就像进入 effects/triggers.rs
并在 event_trigger
函数的开头添加以下内容一样简单(因此它会在伤害之前触发,使伤害指示器仍然出现):
#![allow(unused)] fn main() { fn event_trigger(creator : Option<Entity>, entity: Entity, targets : &Targets, ecs: &mut World) -> bool { let mut did_something = false; let mut gamelog = ecs.fetch_mut::<GameLog>(); // 简单粒子生成 if let Some(part) = ecs.read_storage::<SpawnParticleBurst>().get(entity) { add_effect( creator, EffectType::Particle{ glyph : part.glyph, fg : part.color, bg : rltk::RGB::named(rltk::BLACK), lifespan : part.lifetime_ms }, targets.clone() ); } ... }
线条粒子生成更困难,但还不错。 一个问题是我们实际上不知道物品在哪里! 我们将纠正这一点; 在 effects/targeting.rs
中,我们添加一个新函数:
#![allow(unused)] fn main() { pub fn find_item_position(ecs: &World, target: Entity) -> Option<i32> { let positions = ecs.read_storage::<Position>(); let map = ecs.fetch::<Map>(); // 简单 - 它有一个位置 if let Some(pos) = positions.get(target) { return Some(map.xy_idx(pos.x, pos.y) as i32); } // 也许它被携带了? if let Some(carried) = ecs.read_storage::<InBackpack>().get(target) { if let Some(pos) = positions.get(carried.owner) { return Some(map.xy_idx(pos.x, pos.y) as i32); } } // 也许它被装备了? if let Some(equipped) = ecs.read_storage::<Equipped>().get(target) { if let Some(pos) = positions.get(equipped.owner) { return Some(map.xy_idx(pos.x, pos.y) as i32); } } // 不知道 - 放弃 None } }
此函数首先检查物品是否具有位置(因为它在地面上)。 如果有,则返回它。 然后它查看它是否在背包中; 如果是,它会尝试返回背包所有者的位置。 为装备的物品重复。 如果仍然不知道,则返回 None
。
我们可以将以下内容添加到我们的 event_trigger
函数中,以处理每种目标情况的线条生成:
#![allow(unused)] fn main() { // 线条粒子生成 if let Some(part) = ecs.read_storage::<SpawnParticleLine>().get(entity) { if let Some(start_pos) = targeting::find_item_position(ecs, entity) { match targets { Targets::Tile{tile_idx} => spawn_line_particles(ecs, start_pos, *tile_idx, part), Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| spawn_line_particles(ecs, start_pos, *tile_idx, part)), Targets::Single{ target } => { if let Some(end_pos) = entity_position(ecs, *target) { spawn_line_particles(ecs, start_pos, end_pos, part); } } Targets::TargetList{ targets } => { targets.iter().for_each(|target| { if let Some(end_pos) = entity_position(ecs, *target) { spawn_line_particles(ecs, start_pos, end_pos, part); } }); } } } } }
每种情况都调用 spawn_line_particles
,所以我们也来编写它:
#![allow(unused)] fn main() { fn spawn_line_particles(ecs:&World, start: i32, end: i32, part: &SpawnParticleLine) { let map = ecs.fetch::<Map>(); let start_pt = rltk::Point::new(start % map.width, end / map.width); let end_pt = rltk::Point::new(end % map.width, end / map.width); let line = rltk::line2d(rltk::LineAlg::Bresenham, start_pt, end_pt); for pt in line.iter() { add_effect( None, EffectType::Particle{ glyph : part.glyph, fg : part.color, bg : rltk::RGB::named(rltk::BLACK), lifespan : part.lifetime_ms }, Targets::Tile{ tile_idx : map.xy_idx(pt.x, pt.y) as i32} ); } } }
这非常简单:它在起点和终点之间绘制一条线,并在每个瓦片上放置一个粒子。
您现在可以 cargo run
并享受火球和魔法飞弹的效果。
总结
这是一个巨大的章节,其中包含表面上没有做太多事情的更改。 但是,我们已经获得了很大的收获:
- 物品栏系统现在易于理解。
- 通用效果系统现在可以将任何效果应用于物品或触发器,并且可以轻松扩展新物品,而不会遇到
Specs
限制。 - 责任分配减少了很多:系统不再需要记住显示伤害粒子,甚至不需要了解粒子如何工作 - 它们只是请求它们。 系统通常可以不必担心位置,并以一致的方式应用位置效果(包括 AoE)。
- 我们现在拥有足够灵活的系统,可以让我们构建大型、有凝聚力的效果 - 而无需过多担心细节。
本章很好地说明了 ECS 的局限性 - 以及如何利用它来发挥您的优势。 通过使用组件作为标志,我们可以轻松地组合效果 - 可以治愈您并使您感到困惑的药水就像组合两个标签一样简单。 但是,Specs
实际上与一次读取大量数据存储的系统配合不好 - 因此我们通过在系统之上添加消息传递来解决它。 这很常见:即使是基于 ECS 的引擎 Amethyst 也为此目的实现了消息传递系统。
...
本章的源代码可以在此处找到
使用 Web Assembly 在浏览器中运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。