添加饥饿时钟和食物


关于本教程

本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!

如果您喜欢这个教程并希望我继续创作,请考虑支持我的 Patreon

Hands-On Rust


饥饿时钟是许多 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.rssaveload_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,并点击等待很多次 - 您就会饿死。

Screenshot

显示状态

如果能知道您的饥饿状态就太好了! 我们将修改 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 您的项目,这将提供一个非常令人愉悦的显示: Screenshot

添加食物

饿死固然不错,但是如果玩家在 620 回合后总是开始死亡(并且在此之前会遭受后果!620 听起来可能很多,但是在一个关卡上使用几百步是很常见的,而且我们并没有试图使食物成为主要的游戏焦点),玩家会感到沮丧。 我们将引入一个新的物品 Rations(口粮)。 我们已经拥有了大部分所需的组件,但是我们需要一个新的组件来指示物品 ProvidesFood(提供食物)。 在 components.rs 中:

#![allow(unused)]
fn main() {
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct ProvidesFood {}
}

与往常一样,我们需要在 main.rssaveload_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,您可以四处奔跑 - 找到口粮,并吃掉它们以重置饥饿时钟!

Screenshot

为吃饱喝足添加奖励

如果 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。