为玩家装备物品
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
现在我们有了一个难度不断增加的地下城,是时候开始为玩家提供一些提高他们能力的方法了! 在本章中,我们将从最基本的人类任务开始:装备武器和盾牌。
添加一些可以穿戴/挥舞的物品
我们已经有了很多物品系统的基础,所以我们将基于前几章的基础继续构建。 仅使用我们已经拥有的组件,我们可以在 spawners.rs
中从以下内容开始:
#![allow(unused)] fn main() { fn dagger(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 : "Dagger".to_string() }) .with(Item{}) .marked::<SimpleMarker<SerializeMe>>() .build(); } fn shield(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 : "Shield".to_string() }) .with(Item{}) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
在这两种情况下,我们都在创建一个新实体。 我们给它一个 Position
,因为它必须从地图上的某个位置开始。 我们分配一个 Renderable
,设置为适当的 CP437/ASCII 字形。 我们给它们一个名称,并将它们标记为物品。 我们可以像这样将它们添加到生成表 (spawn table) 中:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1 + map_depth) .add("Health Potion", 7) .add("Fireball Scroll", 2 + map_depth) .add("Confusion Scroll", 2 + map_depth) .add("Magic Missile Scroll", 4) .add("Dagger", 3) .add("Shield", 3) } }
我们也可以很容易地将它们包含在实际生成它们的系统中:
#![allow(unused)] fn main() { // 实际生成怪物 for spawn in spawn_points.iter() { let x = (*spawn.0 % MAPWIDTH) as i32; let y = (*spawn.0 / MAPWIDTH) as i32; match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y), "Orc" => orc(ecs, x, y), "Health Potion" => health_potion(ecs, x, y), "Fireball Scroll" => fireball_scroll(ecs, x, y), "Confusion Scroll" => confusion_scroll(ecs, x, y), "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y), "Dagger" => dagger(ecs, x, y), "Shield" => shield(ecs, x, y), _ => {} } } }
如果你现在 cargo run
项目,你可以四处奔跑并最终找到匕首或盾牌。 在你测试时,你可以考虑将生成频率从 3 提高到一个非常大的数字! 由于我们添加了 Item
标签,你可以在找到这些物品时捡起和放下它们。
装备物品
如果你不能使用匕首和盾牌,它们就不是很有用! 所以让我们让它们可装备。
Equippable 组件
我们需要一种方法来指示物品可以被装备。 你可能已经猜到了,但我们添加了一个新组件! 在 components.rs
中,我们添加:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] pub enum EquipmentSlot { Melee, Shield } #[derive(Component, Serialize, Deserialize, Clone)] pub struct Equippable { pub slot : EquipmentSlot } }
既然我们有了序列化支持(来自第 11 章),我们也必须记住在几个地方注册它。 在 main.rs
中,我们将其添加到已注册组件的列表中:
#![allow(unused)] fn main() { gs.ecs.register::<Equippable>(); }
在 saveload_system.rs
中,我们将其添加到两组组件列表中:
#![allow(unused)] fn main() { serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster, Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, WantsToDropItem, SerializationHelper, Equippable ); }
#![allow(unused)] fn main() { deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, Name, BlocksTile, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, WantsToDropItem, SerializationHelper, Equippable ); }
最后,我们应该将 Equippable
组件添加到 spawners.rs
中的 dagger
和 shield
函数中:
#![allow(unused)] fn main() { fn dagger(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 : "Dagger".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Melee }) .marked::<SimpleMarker<SerializeMe>>() .build(); } fn shield(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 : "Shield".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Shield }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
使物品可装备
一般来说,背包里放着盾牌没有太大帮助(先不考虑明显的“你是怎么把它塞进去的?”问题 - 像许多游戏一样,我们将忽略这个问题!) - 所以你必须能够选择一个来装备。 我们将首先制作另一个组件 Equipped
。 它的工作方式与 InBackpack
类似 - 它指示实体正在持有它。 与 InBackpack
不同,它将指示正在使用的槽位。 这是 components.rs
中的基本 Equipped
组件:
#![allow(unused)] fn main() { #[derive(Component, ConvertSaveload, Clone)] pub struct Equipped { pub owner : Entity, pub slot : EquipmentSlot } }
与之前一样,我们需要在 main.rs
中注册它,并将其包含在 saveload_system.rs
中的序列化和反序列化列表中。
实际装备物品
现在我们想让装备物品成为可能。 这样做将自动卸下同一槽位中的任何物品。 我们将通过我们已经用于使用物品的相同接口来完成此操作,这样我们就不会到处都有不同的菜单。 打开 inventory_system.rs
,我们将编辑 ItemUseSystem
。 我们将首先扩展我们正在引用的系统列表:
#![allow(unused)] fn main() { impl<'a> System<'a> for ItemUseSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, WriteExpect<'a, GameLog>, ReadExpect<'a, Map>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, ReadStorage<'a, Name>, ReadStorage<'a, Consumable>, ReadStorage<'a, ProvidesHealing>, ReadStorage<'a, InflictsDamage>, WriteStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage>, ReadStorage<'a, AreaOfEffect>, WriteStorage<'a, Confusion>, ReadStorage<'a, Equippable>, WriteStorage<'a, Equipped>, WriteStorage<'a, InBackpack> ); fn run(&mut self, data : Self::SystemData) { let (player_entity, mut gamelog, map, entities, mut wants_use, names, consumables, healing, inflict_damage, mut combat_stats, mut suffer_damage, aoe, mut confused, equippable, mut equipped, mut backpack) = data; }
现在,在获取目标之后,添加以下代码块:
#![allow(unused)] fn main() { // 如果物品是可装备的,那么我们想要装备它 - 并卸下该槽位中的任何其他物品 let item_equippable = equippable.get(useitem.item); match item_equippable { None => {} Some(can_equip) => { let target_slot = can_equip.slot; let target = targets[0]; // 移除目标在物品槽位中已有的任何物品 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)); } } } }
这首先匹配以查看我们是否可以装备该物品。 如果可以,它会查找该物品的目标槽位,并查看该槽位中是否已存在物品。 如果有,则将其移动到背包中。 最后,它将 Equipped
组件添加到物品实体,其中包含所有者(现在是玩家)和适当的槽位。
最后,你可能还记得,当玩家移动到下一层时,我们会删除很多实体。 我们希望将玩家 Equipped
的物品作为保留 ECS 中物品的理由。 在 main.rs
中,我们修改 entities_to_remove_on_level_change
如下:
#![allow(unused)] fn main() { fn entities_to_remove_on_level_change(&mut self) -> Vec<Entity> { let entities = self.ecs.entities(); let player = self.ecs.read_storage::<Player>(); let backpack = self.ecs.read_storage::<InBackpack>(); let player_entity = self.ecs.fetch::<Entity>(); let equipped = self.ecs.read_storage::<Equipped>(); let mut to_delete : Vec<Entity> = Vec::new(); for entity in entities.join() { let mut should_delete = true; // 不要删除玩家 let p = player.get(entity); if let Some(_p) = p { should_delete = false; } // 不要删除玩家的装备 let bp = backpack.get(entity); if let Some(bp) = bp { if bp.owner == *player_entity { should_delete = false; } } let eq = equipped.get(entity); if let Some(eq) = eq { if eq.owner == *player_entity { should_delete = false; } } if should_delete { to_delete.push(entity); } } to_delete } }
如果你现在 cargo run
项目,你可以四处奔跑捡起新物品 - 并且可以装备它们。 它们目前还没有任何作用 - 但至少你可以换入和换出它们。 游戏日志将显示装备和卸下装备。
授予战斗奖励
从逻辑上讲,盾牌应该提供一些针对传入伤害的保护 - 而被匕首刺伤应该比被拳头击中更疼! 为了方便这一点,我们将添加更多组件(现在应该是一首熟悉的歌了)。 在 components.rs
中:
#![allow(unused)] fn main() { #[derive(Component, ConvertSaveload, Clone)] pub struct MeleePowerBonus { pub power : i32 } #[derive(Component, ConvertSaveload, Clone)] pub struct DefenseBonus { pub defense : i32 } }
我们还需要记住在 main.rs
和 saveload_system.rs
中注册它们。 然后我们可以修改 spawner.rs
中的代码,将这些组件添加到正确的物品中:
#![allow(unused)] fn main() { fn dagger(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 : "Dagger".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Melee }) .with(MeleePowerBonus{ power: 2 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } fn shield(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 : "Shield".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Shield }) .with(DefenseBonus{ defense: 1 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
请注意我们是如何将组件添加到每个物品的? 现在我们需要修改 melee_combat_system
以应用这些奖励。 我们通过向我们的系统添加一些额外的 ECS 查询来做到这一点:
#![allow(unused)] fn main() { impl<'a> System<'a> for MeleeCombatSystem { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, WriteExpect<'a, GameLog>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, ReadStorage<'a, CombatStats>, WriteStorage<'a, SufferDamage>, ReadStorage<'a, MeleePowerBonus>, ReadStorage<'a, DefenseBonus>, ReadStorage<'a, Equipped> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut log, mut wants_melee, names, combat_stats, mut inflict_damage, melee_power_bonuses, defense_bonuses, equipped) = data; for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { if stats.hp > 0 { let mut offensive_bonus = 0; for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() { if equipped_by.owner == entity { offensive_bonus += power_bonus.power; } } let target_stats = combat_stats.get(wants_melee.target).unwrap(); if target_stats.hp > 0 { let target_name = names.get(wants_melee.target).unwrap(); let mut defensive_bonus = 0; for (_item_entity, defense_bonus, equipped_by) in (&entities, &defense_bonuses, &equipped).join() { if equipped_by.owner == wants_melee.target { defensive_bonus += defense_bonus.defense; } } let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defense + defensive_bonus)); }
这是一大段代码,所以让我们过一遍:
- 我们已将
MeleePowerBonus
、DefenseBonus
和Equipped
读取器添加到系统中。 - 一旦我们确定攻击者还活着,我们将
offensive_bonus
设置为 0。 - 我们迭代所有具有
MeleePowerBonus
和Equipped
条目的实体。 如果它们是由攻击者装备的,我们将它们的攻击奖励添加到offensive_bonus
。 - 一旦我们确定防御者还活着,我们将
defensive_bonus
设置为 0。 - 我们迭代所有具有
DefenseBonus
和Equipped
条目的实体。 如果它们是由目标装备的,我们将它们的防御添加到defense_bonus
。 - 当我们计算伤害时,我们将攻击奖励添加到攻击方 - 并将防御奖励添加到防御方。
如果你现在 cargo run
,你会发现使用匕首会让你攻击更猛烈 - 而使用盾牌会让你受到的伤害更少。
卸下物品
现在你可以装备物品,并通过交换来移除它们,你可能想要停止持有物品并将其放回背包。 在像这样简单的游戏中,这并不是绝对必要的 - 但这是一个很好的未来选择。 我们将 R
键绑定到 remove 物品,因为该键是可用的。 在 player.rs
中,将此添加到输入代码:
#![allow(unused)] fn main() { VirtualKeyCode::R => return RunState::ShowRemoveItem, }
现在我们在 main.rs
的 RunState
中添加 ShowRemoveItem
:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory, ShowDropItem, ShowTargeting { range : i32, item : Entity}, MainMenu { menu_selection : gui::MainMenuSelection }, SaveGame, NextLevel, ShowRemoveItem, GameOver } }
我们在 tick
中为其添加一个处理程序:
#![allow(unused)] fn main() { RunState::ShowRemoveItem => { let result = gui::remove_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::<WantsToRemoveItem>(); intent.insert(*self.ecs.fetch::<Entity>(), WantsToRemoveItem{ item: item_entity }).expect("Unable to insert intent"); newrunstate = RunState::PlayerTurn; } } } }
我们将在 components.rs
中实现一个新组件(有关序列化处理程序,请参阅源代码;它是想要丢弃物品的处理程序的剪切粘贴,并更改了名称):
#![allow(unused)] fn main() { #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct WantsToRemoveItem { pub item : Entity } }
与往常一样,它必须在 main.rs
和 saveload_system.rs
中注册。
现在在 gui.rs
中,我们将实现 remove_item_menu
。 它几乎与物品丢弃菜单完全相同,但更改了查询内容和标题(在某个时候将这些变成更通用的函数将是一个好主意!):
#![allow(unused)] fn main() { pub fn remove_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::<Equipped>(); 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), "Remove 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) } } } } } }
接下来,我们应该扩展 inventory_system.rs
以支持移除物品。 幸运的是,这是一个非常简单的系统:
#![allow(unused)] fn main() { pub struct ItemRemoveSystem {} impl<'a> System<'a> for ItemRemoveSystem { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, WriteStorage<'a, WantsToRemoveItem>, WriteStorage<'a, Equipped>, WriteStorage<'a, InBackpack> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut wants_remove, mut equipped, mut backpack) = data; for (entity, to_remove) in (&entities, &wants_remove).join() { equipped.remove(to_remove.item); backpack.insert(to_remove.item, InBackpack{ owner: entity }).expect("Unable to insert backpack"); } wants_remove.clear(); } } }
最后,我们将其添加到 main.rs
中的系统中:
#![allow(unused)] fn main() { impl State { fn run_systems(&mut self) { let mut vis = VisibilitySystem{}; vis.run_now(&self.ecs); let mut mob = MonsterAI{}; mob.run_now(&self.ecs); let mut mapindex = MapIndexingSystem{}; mapindex.run_now(&self.ecs); let mut melee = MeleeCombatSystem{}; melee.run_now(&self.ecs); let mut damage = DamageSystem{}; damage.run_now(&self.ecs); let mut pickup = ItemCollectionSystem{}; pickup.run_now(&self.ecs); let mut itemuse = ItemUseSystem{}; itemuse.run_now(&self.ecs); let mut drop_items = ItemDropSystem{}; drop_items.run_now(&self.ecs); let mut item_remove = ItemRemoveSystem{}; item_remove.run_now(&self.ecs); self.ecs.maintain(); } } }
现在,如果你 cargo run
,你可以捡起匕首或盾牌并装备它。 然后你可以按 R
来移除它。
稍后添加更强大的装备
让我们在 spawner.rs
中添加更多物品:
#![allow(unused)] fn main() { fn longsword(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('/'), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Longsword".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Melee }) .with(MeleePowerBonus{ power: 4 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } fn tower_shield(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('('), fg: RGB::named(rltk::YELLOW), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Tower Shield".to_string() }) .with(Item{}) .with(Equippable{ slot: EquipmentSlot::Shield }) .with(DefenseBonus{ defense: 3 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
我们将在 random_table.rs
中添加一个快速修复,以忽略生成几率 (spawn chance) 为 0 或更低的条目:
#![allow(unused)] fn main() { pub fn add<S:ToString>(mut self, name : S, weight: i32) -> RandomTable { if weight > 0 { self.total_weight += weight; self.entries.push(RandomEntry::new(name.to_string(), weight)); } self } }
回到 spawner.rs
,我们将它们添加到战利品表 (loot table) 中 - 并在地下城后期有一定几率出现:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1 + map_depth) .add("Health Potion", 7) .add("Fireball Scroll", 2 + map_depth) .add("Confusion Scroll", 2 + map_depth) .add("Magic Missile Scroll", 4) .add("Dagger", 3) .add("Shield", 3) .add("Longsword", map_depth - 1) .add("Tower Shield", map_depth - 1) } }
#![allow(unused)] fn main() { match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y), "Orc" => orc(ecs, x, y), "Health Potion" => health_potion(ecs, x, y), "Fireball Scroll" => fireball_scroll(ecs, x, y), "Confusion Scroll" => confusion_scroll(ecs, x, y), "Magic Missile Scroll" => magic_missile_scroll(ecs, x, y), "Dagger" => dagger(ecs, x, y), "Shield" => shield(ecs, x, y), "Longsword" => longsword(ecs, x, y), "Tower Shield" => tower_shield(ecs, x, y), _ => {} } }
现在,随着你进一步深入,你可以找到更好的武器和盾牌!
游戏结束画面
我们快要完成基本教程了,所以让我们在您死亡时做一些事情 - 而不是锁定在控制台循环中。 在文件 damage_system.rs
中,我们将编辑 delete_the_dead
中 player
的 match 语句:
#![allow(unused)] fn main() { match player { None => { let victim_name = names.get(entity); if let Some(victim_name) = victim_name { log.entries.push(format!("{} is dead", &victim_name.name)); } dead.push(entity) } Some(_) => { let mut runstate = ecs.write_resource::<RunState>(); *runstate = RunState::GameOver; } } }
当然,我们现在必须去 main.rs
并添加新状态:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory, ShowDropItem, ShowTargeting { range : i32, item : Entity}, MainMenu { menu_selection : gui::MainMenuSelection }, SaveGame, NextLevel, ShowRemoveItem, GameOver } }
我们将其添加到状态实现中,也在 main.rs
中:
#![allow(unused)] fn main() { RunState::GameOver => { let result = gui::game_over(ctx); match result { gui::GameOverResult::NoSelection => {} gui::GameOverResult::QuitToMenu => { self.game_over_cleanup(); newrunstate = RunState::MainMenu{ menu_selection: gui::MainMenuSelection::NewGame }; } } } }
这相对简单:我们调用 game_over
来渲染菜单,当您退出时,我们删除 ECS 中的所有内容。 最后,在 gui.rs
中,我们将实现 game_over
:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum GameOverResult { NoSelection, QuitToMenu } pub fn game_over(ctx : &mut Rltk) -> GameOverResult { ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Your journey has ended!"); ctx.print_color_centered(17, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "One day, we'll tell you all about how you did."); ctx.print_color_centered(18, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "That day, sadly, is not in this chapter.."); ctx.print_color_centered(20, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Press any key to return to the menu."); match ctx.key { None => GameOverResult::NoSelection, Some(_) => GameOverResult::QuitToMenu } } }
最后,我们将处理 game_over_cleanup
:
#![allow(unused)] fn main() { fn game_over_cleanup(&mut self) { // 删除所有内容 let mut to_delete = Vec::new(); for e in self.ecs.entities().join() { to_delete.push(e); } for del in to_delete.iter() { self.ecs.delete_entity(*del).expect("Deletion failed"); } // 构建新地图并放置玩家 let worldmap; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); *worldmap_resource = Map::new_map_rooms_and_corridors(1); worldmap = worldmap_resource.clone(); } // 生成坏人 for room in worldmap.rooms.iter().skip(1) { spawner::spawn_room(&mut self.ecs, room, 1); } // 放置玩家并更新资源 let (player_x, player_y) = worldmap.rooms[0].center(); let player_entity = spawner::player(&mut self.ecs, player_x, player_y); let mut player_position = self.ecs.write_resource::<Point>(); *player_position = Point::new(player_x, player_y); let mut position_components = self.ecs.write_storage::<Position>(); let mut player_entity_writer = self.ecs.write_resource::<Entity>(); *player_entity_writer = player_entity; let player_pos_comp = position_components.get_mut(player_entity); if let Some(player_pos_comp) = player_pos_comp { player_pos_comp.x = player_x; player_pos_comp.y = player_y; } // 将玩家的视野标记为脏 (dirty) let mut viewshed_components = self.ecs.write_storage::<Viewshed>(); let vs = viewshed_components.get_mut(player_entity); if let Some(vs) = vs { vs.dirty = true; } } }
从我们加载游戏时的序列化工作中,这应该看起来很熟悉。 它非常相似,但它生成了一个新玩家。
现在,如果你 cargo run
,并且死亡 - 你将收到一条消息,告知你游戏结束,并将你送回菜单。
总结
这是本教程第一部分的结尾。 它相对紧密地遵循了 Python 教程,并将您从“hello rust”带到一个相当有趣的 Roguelike 游戏。 我希望你喜欢它! 请继续关注,我希望很快添加第二部分。
本章的源代码可以在这里找到
在你的浏览器中使用 web assembly 运行本章的示例 (需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson.