添加饥饿时钟和食物
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续创作,请考虑支持我的 Patreon。
饥饿时钟是许多 Roguelike 游戏中备受争议的功能。 如果您花费所有时间寻找食物,它们真的会困扰玩家,但它们也会推动您前进 - 因此您不能无所事事而不去探索更多。 尤其,休息以恢复生命值变成了一个更具风险/回报的系统。 本章将为玩家实现一个基本的饥饿时钟。
添加饥饿时钟组件
我们将为玩家添加一个饥饿时钟,因此第一步是创建一个组件来表示它。 在 components.rs
中:
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Copy, Clone, PartialEq)] pub enum HungerState { WellFed, Normal, Hungry, Starving } #[derive(Component, Serialize, Deserialize, Clone)] pub struct HungerClock { pub state : HungerState, pub duration : i32 } }
与所有组件一样,它需要在 main.rs
和 saveload_system.rs
中注册。 在 spawners.rs
中,我们将扩展 player
函数,为玩家添加饥饿时钟:
#![allow(unused)] fn main() { 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), render_order: 0 }) .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 }) .with(HungerClock{ state: HungerState::WellFed, duration: 20 }) .marked::<SimpleMarker<SerializeMe>>() .build() } }
现在已经有了一个饥饿时钟组件,但它还没有任何作用!
添加饥饿系统
我们将创建一个新文件 hunger_system.rs
并实现一个饥饿时钟系统。 这非常简单直接:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{HungerClock, RunState, HungerState, SufferDamage, gamelog::GameLog}; pub struct HungerSystem {} impl<'a> System<'a> for HungerSystem { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, WriteStorage<'a, HungerClock>, ReadExpect<'a, Entity>, // The player ReadExpect<'a, RunState>, WriteStorage<'a, SufferDamage>, WriteExpect<'a, GameLog> ); fn run(&mut self, data : Self::SystemData) { let (entities, mut hunger_clock, player_entity, runstate, mut inflict_damage, mut log) = data; for (entity, mut clock) in (&entities, &mut hunger_clock).join() { let mut proceed = false; match *runstate { RunState::PlayerTurn => { if entity == *player_entity { proceed = true; } } RunState::MonsterTurn => { if entity != *player_entity { proceed = true; } } _ => proceed = false } if proceed { clock.duration -= 1; if clock.duration < 1 { match clock.state { HungerState::WellFed => { clock.state = HungerState::Normal; clock.duration = 200; if entity == *player_entity { log.entries.push("You are no longer well fed.".to_string()); } } HungerState::Normal => { clock.state = HungerState::Hungry; clock.duration = 200; if entity == *player_entity { log.entries.push("You are hungry.".to_string()); } } HungerState::Hungry => { clock.state = HungerState::Starving; clock.duration = 200; if entity == *player_entity { log.entries.push("You are starving!".to_string()); } } HungerState::Starving => { // Inflict damage from hunger if entity == *player_entity { log.entries.push("Your hunger pangs are getting painful! You suffer 1 hp damage.".to_string()); } SufferDamage::new_damage(&mut inflict_damage, entity, 1); } } } } } } } }
它的工作原理是迭代所有拥有 HungerClock
的实体。 如果它们是玩家,则仅在 PlayerTurn
状态下生效; 同样,如果它们是怪物,则仅在它们的 turn 中发生(以防我们以后想要饥饿的怪物!)。 当前状态的持续时间在每次运行时都会减少。 如果它达到 0,它会向下移动一个状态 - 或者如果您处于饥饿状态,则会对您造成伤害。
现在我们需要将其添加到 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); let mut hunger = hunger_system::HungerSystem{}; hunger.run_now(&self.ecs); let mut particles = particle_system::ParticleSpawnSystem{}; particles.run_now(&self.ecs); self.ecs.maintain(); } } }
如果您现在 cargo run
,并点击等待很多次 - 您就会饿死。
显示状态
如果能知道您的饥饿状态就太好了! 我们将修改 gui.rs
中的 draw_ui
来显示它:
#![allow(unused)] fn main() { pub fn draw_ui(ecs: &World, ctx : &mut Rltk) { ctx.draw_box(0, 43, 79, 6, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); let combat_stats = ecs.read_storage::<CombatStats>(); let players = ecs.read_storage::<Player>(); let hunger = ecs.read_storage::<HungerClock>(); for (_player, stats, hc) in (&players, &combat_stats, &hunger).join() { let health = format!(" HP: {} / {} ", stats.hp, stats.max_hp); ctx.print_color(12, 43, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), &health); ctx.draw_bar_horizontal(28, 43, 51, stats.hp, stats.max_hp, RGB::named(rltk::RED), RGB::named(rltk::BLACK)); match hc.state { HungerState::WellFed => ctx.print_color(71, 42, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Well Fed"), HungerState::Normal => {} HungerState::Hungry => ctx.print_color(71, 42, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "Hungry"), HungerState::Starving => ctx.print_color(71, 42, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "Starving"), } } ... }
如果您 cargo run
您的项目,这将提供一个非常令人愉悦的显示:
添加食物
饿死固然不错,但是如果玩家在 620 回合后总是开始死亡(并且在此之前会遭受后果!620 听起来可能很多,但是在一个关卡上使用几百步是很常见的,而且我们并没有试图使食物成为主要的游戏焦点),玩家会感到沮丧。 我们将引入一个新的物品 Rations
(口粮)。 我们已经拥有了大部分所需的组件,但是我们需要一个新的组件来指示物品 ProvidesFood
(提供食物)。 在 components.rs
中:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct ProvidesFood {} }
与往常一样,我们需要在 main.rs
和 saveload_system.rs
中注册它。
现在,在 spawner.rs
中,我们将创建一个新函数来制作口粮:
#![allow(unused)] fn main() { fn rations(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('%'), fg: RGB::named(rltk::GREEN), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Rations".to_string() }) .with(Item{}) .with(ProvidesFood{}) .with(Consumable{}) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
我们还将它添加到生成表(非常常见):
#![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) .add("Rations", 10) } }
并添加到生成代码:
#![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), "Rations" => rations(ecs, x, y), _ => {} } }
如果您现在 cargo run
,您将遇到可以捡起和丢弃的口粮。 但是,您不能吃它们! 我们将把该功能添加到 inventory_system.rs
中。 这是相关的部分(完整版本请参见教程源代码):
#![allow(unused)] fn main() { // It it is edible, eat it! // 如果它是可食用的,就吃掉它! let item_edible = provides_food.get(useitem.item); match item_edible { None => {} Some(_) => { used_item = true; let target = targets[0]; let hc = hunger_clocks.get_mut(target); if let Some(hc) = hc { hc.state = HungerState::WellFed; hc.duration = 20; gamelog.entries.push(format!("You eat the {}.", names.get(useitem.item).unwrap().name)); } } } }
如果您现在 cargo run
,您可以四处奔跑 - 找到口粮,并吃掉它们以重置饥饿时钟!
为吃饱喝足添加奖励
如果 Well Fed
(吃饱喝足)状态能做些什么就太好了! 当您吃饱时,我们将给您一个临时的 +1 力量奖励。 这鼓励玩家进食 - 即使他们不必这样做(偷偷地使在较低级别生存更加困难,因为食物变得不那么丰富)。 在 melee_combat_system.rs
中,我们添加:
#![allow(unused)] fn main() { let hc = hunger_clock.get(entity); if let Some(hc) = hc { if hc.state == HungerState::WellFed { offensive_bonus += 1; } } }
就是这样! 您会因为吃饱口粮而获得 +1 力量奖励。
防止在饥饿或饥饿状态下治疗
作为食物的另一个好处,我们将阻止您在饥饿或饥饿状态下等待治疗(这也平衡了我们之前添加的治疗系统)。 在 player.rs
中,我们修改 skip_turn
:
#![allow(unused)] fn main() { let hunger_clocks = ecs.read_storage::<HungerClock>(); let hc = hunger_clocks.get(*player_entity); if let Some(hc) = hc { match hc.state { HungerState::Hungry => can_heal = false, HungerState::Starving => can_heal = false, _ => {} } } if can_heal { }
总结
我们现在有了一个可用的饥饿时钟系统。 您可能想要调整持续时间以适合您的口味(或者如果它不是您的菜,则完全跳过它) - 但它是该类型的支柱,因此最好将其包含在教程中。
本章的源代码可以在这里找到
在您的浏览器中使用 WebAssembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。