远程卷轴和瞄准
关于本教程
本教程是免费且开源的,所有代码使用 MIT 许可证——因此您可以随意使用它。我希望您会喜欢这个教程,并制作出很棒的游戏!
如果你喜欢这个并且希望我继续写作,请考虑支持我的 Patreon。
在上一章中,我们添加了物品和库存——以及一种物品类型,即生命药水。现在我们将添加第二种物品类型:魔法飞弹卷轴,它可以让你远程攻击实体。
使用组件来描述物品的功能
在上一章中,我们基本上编写了代码以确保所有物品都是治疗药水。这使事情开始运转,但不是很灵活。因此,我们将从将物品分解为更多组件类型开始。我们将从一个简单的标志组件Consumable
开始:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct Consumable {} }
拥有此物品表示使用它会销毁它(使用时消耗)。因此,我们将 PotionUseSystem
(我们将其重命名为 ItemUseSystem
!)中总是调用的 entities.delete(useitem.item).expect("Delete failed");
替换为:
#![allow(unused)] fn main() { let consumable = consumables.get(useitem.item); match consumable { None => {} Some(_) => { entities.delete(useitem.item).expect("Delete failed"); } } }
这很简单:检查组件是否有Consumable
标签,如果有就销毁它。同样,我们可以用ProvidesHealing
替换Potion
部分,以表明这是药水实际的作用。在components.rs
中:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct ProvidesHealing { pub heal_amount : i32 } }
在我们的 ItemUseSystem
中:
#![allow(unused)] fn main() { let item_heals = healing.get(useitem.item); match item_heals { None => {} Some(healer) => { stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); if entity == *player_entity { gamelog.entries.push(format!("You drink the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount)); } } } }
综合起来,我们创建药水的代码(在 spawner.rs
中)如下所示:
#![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), render_order: 2 }) .with(Name{ name : "Health Potion".to_string() }) .with(Item{}) .with(Consumable{}) .with(ProvidesHealing{ heal_amount: 8 }) .build(); } }
所以我们描述了它的位置、外观、名称,表示它是一个物品,使用时会被消耗,并提供 8 点治疗。这描述得很详细——未来的物品可以混合/匹配。随着我们添加组件,物品系统将变得越来越灵活。
描述远程魔法飞弹卷轴
我们需要添加一些更多的组件!在 components.rs
(并在 main.rs
中注册):
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct Ranged { pub range : i32 } #[derive(Component, Debug)] pub struct InflictsDamage { pub damage : i32 } }
这反过来让我们可以在 spawner.rs
中编写一个 magic_missile_scroll
函数,该函数有效地描述了卷轴:
#![allow(unused)] fn main() { fn magic_missile_scroll(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437(')'), fg: RGB::named(rltk::CYAN), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Magic Missile Scroll".to_string() }) .with(Item{}) .with(Consumable{}) .with(Ranged{ range: 6 }) .with(InflictsDamage{ damage: 8 }) .build(); } }
这清楚地列出了它的特性:它有一个位置,一个外观,一个名字,它是一个使用后会被销毁的物品,它的范围是 6 个格子,并造成 8 点伤害。这就是我喜欢组件的地方:过了一段时间,听起来更像是在描述一个设备的蓝图,而不是写很多行代码!
我们将继续将它们添加到生成列表中:
#![allow(unused)] fn main() { fn random_item(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 => { health_potion(ecs, x, y) } _ => { magic_missile_scroll(ecs, x, y) } } } }
在物品生成代码中,将调用 health_potion
替换为调用 random_item
。
如果你现在运行程序(使用 cargo run
),你会发现周围有卷轴和药水。组件系统已经提供了相当多的功能:
- 你可以在地图上看到它们(感谢
Renderable
和Position
) - 你可以捡起它们并放下它们(感谢
Item
) - 你可以在你的库存中列出它们
- 你可以对它们调用
use
,它们会被销毁:但什么也不会发生。
为物品实现范围伤害
我们希望魔法飞弹能够被瞄准:你激活它,然后必须选择一个受害者。这将是一种新的输入模式,所以我们再次在 main.rs
中扩展 RunState
:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory, ShowDropItem, ShowTargeting { range : i32, item : Entity} } }
我们将在 main.rs
中扩展 ShowInventory
的处理程序,以处理远程物品并引发模式切换:
#![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 is_ranged = self.ecs.read_storage::<Ranged>(); let is_item_ranged = is_ranged.get(item_entity); if let Some(is_item_ranged) = is_item_ranged { newrunstate = RunState::ShowTargeting{ range: is_item_ranged.range, item: item_entity }; } else { let mut intent = self.ecs.write_storage::<WantsToUseItem>(); intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{ item: item_entity, target: None }).expect("Unable to insert intent"); newrunstate = RunState::PlayerTurn; } } } } }
所以在 main.rs
中,当我们匹配适当的游戏模式时,我们可以暂时插入:
#![allow(unused)] fn main() { RunState::ShowTargeting{range, item} => { let target = gui::ranged_target(self, ctx, range); } }
这自然会导致实际编写 gui::ranged_target
。这看起来很复杂,但实际上非常简单:
#![allow(unused)] fn main() { pub fn ranged_target(gs : &mut State, ctx : &mut Rltk, range : i32) -> (ItemMenuResult, Option<Point>) { let player_entity = gs.ecs.fetch::<Entity>(); let player_pos = gs.ecs.fetch::<Point>(); let viewsheds = gs.ecs.read_storage::<Viewshed>(); ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Select Target:"); // Highlight available target cells let mut available_cells = Vec::new(); let visible = viewsheds.get(*player_entity); if let Some(visible) = visible { // We have a viewshed for idx in visible.visible_tiles.iter() { let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx); if distance <= range as f32 { ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE)); available_cells.push(idx); } } } else { return (ItemMenuResult::Cancel, None); } // Draw mouse cursor let mouse_pos = ctx.mouse_pos(); let mut valid_target = false; for idx in available_cells.iter() { if idx.x == mouse_pos.0 && idx.y == mouse_pos.1 { valid_target = true; } } if valid_target { ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN)); if ctx.left_click { return (ItemMenuResult::Selected, Some(Point::new(mouse_pos.0, mouse_pos.1))); } } else { ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED)); if ctx.left_click { return (ItemMenuResult::Cancel, None); } } (ItemMenuResult::NoResponse, None) } }
所以我们首先获取玩家的位置和视野,并迭代他们可以看到的单元格。我们检查单元格的范围与物品的范围,如果在范围内,我们将单元格高亮为蓝色。我们还维护一个可能目标单元格的列表。然后,我们获取鼠标位置;如果指向有效目标,我们将其高亮为青色——否则使用红色。如果你点击一个有效单元格,它会返回你瞄准的目标信息——否则,它会取消。
现在我们将 ShowTargeting
代码扩展以处理这种情况:
#![allow(unused)] fn main() { RunState::ShowTargeting{range, item} => { let result = gui::ranged_target(self, ctx, range); match result.0 { gui::ItemMenuResult::Cancel => newrunstate = RunState::AwaitingInput, gui::ItemMenuResult::NoResponse => {} gui::ItemMenuResult::Selected => { let mut intent = self.ecs.write_storage::<WantsToUseItem>(); intent.insert(*self.ecs.fetch::<Entity>(), WantsToUseItem{ item, target: result.1 }).expect("Unable to insert intent"); newrunstate = RunState::PlayerTurn; } } } }
这是什么target
?我在components.rs
中的WantsToUseItem
添加了另一个字段:
#![allow(unused)] fn main() { #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct WantsToUseItem { pub item : Entity, pub target : Option<rltk::Point> } }
所以现在当你收到一个 WantsToUseItem
时,你可以知道 用户 是拥有实体,物品 是 item
字段,并且它瞄准的是 target
- 如果有的话(瞄准对治疗药水来说没有太大意义!)。
所以现在我们可以为我们的 ItemUseSystem
添加另一个条件:
#![allow(unused)] fn main() { // If it inflicts damage, apply it to the target cell let item_damages = inflict_damage.get(useitem.item); match item_damages { None => {} Some(damage) => { let target_point = useitem.target.unwrap(); let idx = map.xy_idx(target_point.x, target_point.y); used_item = false; for mob in map.tile_content[idx].iter() { SufferDamage::new_damage(&mut suffer_damage, *mob, damage.damage); if entity == *player_entity { let mob_name = names.get(*mob).unwrap(); let item_name = names.get(useitem.item).unwrap(); gamelog.entries.push(format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage)); } used_item = true; } } } }
这检查物品上是否有造成伤害
组件——如果有,则对目标单元格内的所有人施加伤害。
如果你运行游戏,你现在可以用你的魔法飞弹卷轴攻击实体!
介绍效果范围
我们将添加另一种滚动类型 - 火球。这是一个老牌经典,并引入了 AoE - 区域效果 - 伤害。我们将首先添加一个组件来表明我们的意图:
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct AreaOfEffect { pub radius : i32 } }
我们将扩展spawner.rs
中的random_item
函数,使其成为一个选项:
#![allow(unused)] fn main() { fn random_item(ecs: &mut World, x: i32, y: i32) { let roll :i32; { let mut rng = ecs.write_resource::<RandomNumberGenerator>(); roll = rng.roll_dice(1, 3); } match roll { 1 => { health_potion(ecs, x, y) } 2 => { fireball_scroll(ecs, x, y) } _ => { magic_missile_scroll(ecs, x, y) } } } }
所以现在我们可以编写一个fireball_scroll
函数来实际生成它们。这与其他物品非常相似:
#![allow(unused)] fn main() { fn fireball_scroll(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437(')'), fg: RGB::named(rltk::ORANGE), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Fireball Scroll".to_string() }) .with(Item{}) .with(Consumable{}) .with(Ranged{ range: 6 }) .with(InflictsDamage{ damage: 20 }) .with(AreaOfEffect{ radius: 3 }) .build(); } }
请注意,基本上是一样的——但我们正在添加一个AreaOfEffect
组件,以表明这是我们想要的。如果你现在运行cargo run
,你会在游戏中看到火球卷轴——它们会对单个实体造成伤害。显然,我们必须解决这个问题!
在我们的 UseItemSystem
中,我们将构建一个新部分来确定效果的目标列表:
#![allow(unused)] fn main() { // Targeting let mut targets : Vec<Entity> = Vec::new(); match useitem.target { None => { targets.push( *player_entity ); } Some(target) => { let area_effect = aoe.get(useitem.item); match area_effect { None => { // Single target in tile let idx = map.xy_idx(target.x, target.y); for mob in map.tile_content[idx].iter() { targets.push(*mob); } } Some(area_effect) => { // AoE let mut blast_tiles = rltk::field_of_view(target, area_effect.radius, &*map); blast_tiles.retain(|p| p.x > 0 && p.x < map.width-1 && p.y > 0 && p.y < map.height-1 ); for tile_idx in blast_tiles.iter() { let idx = map.xy_idx(tile_idx.x, tile_idx.y); for mob in map.tile_content[idx].iter() { targets.push(*mob); } } } } } } }
这表示“如果没有目标,则将其应用于玩家”。如果有目标,检查它是否是范围效果事件;如果是 - 从该点绘制适当半径的视野,并添加目标区域中的每个实体。如果不是,我们只需获取目标图块中的实体。
现在我们需要使效果代码通用化。我们不想假设效果是独立的;以后我们可能会决定用卷轴攻击某物会产生各种效果!所以对于治疗,它看起来像这样:
#![allow(unused)] fn main() { // If it heals, apply the healing let item_heals = healing.get(useitem.item); match item_heals { None => {} Some(healer) => { for target in targets.iter() { let stats = combat_stats.get_mut(*target); if let Some(stats) = stats { stats.hp = i32::min(stats.max_hp, stats.hp + healer.heal_amount); if entity == *player_entity { gamelog.entries.push(format!("You use the {}, healing {} hp.", names.get(useitem.item).unwrap().name, healer.heal_amount)); } } } } } }
伤害代码实际上已经简化,因为我们已经计算了目标:
#![allow(unused)] fn main() { // If it inflicts damage, apply it to the target cell let item_damages = inflict_damage.get(useitem.item); match item_damages { None => {} Some(damage) => { used_item = false; for mob in targets.iter() { SufferDamage::new_damage(&mut suffer_damage, *mob, damage.damage); if entity == *player_entity { let mob_name = names.get(*mob).unwrap(); let item_name = names.get(useitem.item).unwrap(); gamelog.entries.push(format!("You use {} on {}, inflicting {} hp.", item_name.name, mob_name.name, damage.damage)); } used_item = true; } } } }
如果你现在 cargo run
这个项目,你可以使用魔法飞弹卷轴、火球卷轴和治疗药水。
混乱卷轴
让我们添加另一个物品 - 混乱卷轴。这些将对范围内的单个实体生效,并使它们在几个回合内陷入混乱 - 在此期间它们将什么也不做。我们将从描述我们希望在物品生成代码中实现的内容开始:
#![allow(unused)] fn main() { fn confusion_scroll(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437(')'), fg: RGB::named(rltk::PINK), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Confusion Scroll".to_string() }) .with(Item{}) .with(Consumable{}) .with(Ranged{ range: 6 }) .with(Confusion{ turns: 4 }) .build(); } }
我们也会将其添加到选项中:
#![allow(unused)] fn main() { fn random_item(ecs: &mut World, x: i32, y: i32) { let roll :i32; { let mut rng = ecs.write_resource::<RandomNumberGenerator>(); roll = rng.roll_dice(1, 4); } match roll { 1 => { health_potion(ecs, x, y) } 2 => { fireball_scroll(ecs, x, y) } 3 => { confusion_scroll(ecs, x, y) } _ => { magic_missile_scroll(ecs, x, y) } } } }
我们将添加一个新组件(并注册它!):
#![allow(unused)] fn main() { #[derive(Component, Debug)] pub struct Confusion { pub turns : i32 } }
这足以使它们出现、可触发并导致目标选择发生——但使用时不会发生任何事情。我们将添加将混乱传递给 ItemUseSystem
的能力:
#![allow(unused)] fn main() { // Can it pass along confusion? Note the use of scopes to escape from the borrow checker! let mut add_confusion = Vec::new(); { let causes_confusion = confused.get(useitem.item); match causes_confusion { None => {} Some(confusion) => { used_item = false; for mob in targets.iter() { add_confusion.push((*mob, confusion.turns )); if entity == *player_entity { let mob_name = names.get(*mob).unwrap(); let item_name = names.get(useitem.item).unwrap(); gamelog.entries.push(format!("You use {} on {}, confusing them.", item_name.name, mob_name.name)); } } } } } for mob in add_confusion.iter() { confused.insert(mob.0, Confusion{ turns: mob.1 }).expect("Unable to insert status"); } }
好的!现在我们可以将 Confused
状态添加到任何东西上。我们应该更新 monster_ai_system
以使用它。将循环替换为:
#![allow(unused)] fn main() { for (entity, mut viewshed,_monster,mut pos) in (&entities, &mut viewshed, &monster, &mut position).join() { let mut can_act = true; let is_confused = confused.get_mut(entity); if let Some(i_am_confused) = is_confused { i_am_confused.turns -= 1; if i_am_confused.turns < 1 { confused.remove(entity); } can_act = false; } if can_act { let distance = rltk::DistanceAlg::Pythagoras.distance2d(Point::new(pos.x, pos.y), *player_pos); if distance < 1.5 { wants_to_melee.insert(entity, WantsToMelee{ target: *player_entity }).expect("Unable to insert attack"); } else if viewshed.visible_tiles.contains(&*player_pos) { // Path to the player let path = rltk::a_star_search( map.xy_idx(pos.x, pos.y), map.xy_idx(player_pos.x, player_pos.y), &mut *map ); if path.success && path.steps.len()>1 { let mut idx = map.xy_idx(pos.x, pos.y); map.blocked[idx] = false; pos.x = path.steps[1] as i32 % map.width; pos.y = path.steps[1] as i32 / map.width; idx = map.xy_idx(pos.x, pos.y); map.blocked[idx] = true; viewshed.dirty = true; } } } } }
如果看到一个Confused
组件,它会减少计时器。如果计时器达到 0,它会移除它。然后返回,使怪物跳过它的回合。
本章的源代码可以在此处找到这里
运行本章的示例与 Web Assembly,在您的浏览器中(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.
版权 (C) 2024, myedgetech.com.