经验与升级


关于本教程

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

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

Hands-On Rust


到目前为止,我们已经深入地下城,并且实际上只通过找到更好的装备来提升了自己。 属性更好的剑、盾牌和盔甲 - 使我们有机会在更强大的敌人面前生存下来。 这很好,但这通常只是 Roguelike 和 RPG 中常见等式的一半; 击败敌人通常会奖励 经验值 - 这些经验值可以用来提升你的角色。

我们正在制作的游戏类型暗示了一些指导原则:

  • 由于永久死亡的设定,你可以预料到会 死很多次。 因此,管理你的角色进度需要 简单 - 这样你就不会花费大量时间在上面,结果却不得不重新再来一次。
  • 垂直进度 是一件好事:随着你深入地下城,你会变得更强大(这允许我们制作更强大的怪物)。 水平 进度在很大程度上破坏了永久死亡的意义; 如果你在游戏之间保留收益,那么 Roguelike 游戏中“每次游戏都是独一无二的”这一方面就会受到损害,你可以预料到 Reddit 上的 /r/roguelikes 的伙伴们会抱怨!

获取经验值

当你击败某些东西时,你应该获得经验值(XP)。 我们现在采用一个简单的升级方案:每次你击败某些东西时,你都会获得 100 XP * 敌人等级。 这对于杀死强大的敌人来说收益更大 - 而对于猎杀那些你已经超越等级的生物来说,收益则相对较小。 此外,我们决定你需要 1,000 XP * 当前等级 才能升到下一级。

我们的 Pools 组件中已经有了 levelxp (你几乎会认为我们正在计划这一章!)。 让我们从修改我们的 GUI 来显示等级进度开始。 打开 gui.rs,我们将以下内容添加到 draw_ui

#![allow(unused)]
fn main() {
format!("Level:  {}", player_pools.level);
ctx.print_color(50, 3, white, black, &xp);
let xp_level_start = (player_pools.level-1) * 1000;
ctx.draw_bar_horizontal(64, 3, 14, player_pools.xp - xp_level_start, 1000, RGB::named(rltk::GOLD), RGB::named(rltk::BLACK));
}

这会在屏幕上为我们当前的等级进度添加一个金色的进度条,并显示我们当前的等级:

Screenshot

现在我们应该支持实际 获得 经验值。 我们应该从跟踪伤害 来自哪里 开始。 打开 components.rs,我们将在 SufferDamage 中添加一个字段:

#![allow(unused)]
fn main() {
#[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct SufferDamage {
    pub amount : Vec<(i32, bool)>,
}

impl SufferDamage {
    pub fn new_damage(store: &mut WriteStorage<SufferDamage>, victim: Entity, amount: i32, from_player: bool) {
        if let Some(suffering) = store.get_mut(victim) {
            suffering.amount.push((amount, from_player));
        } else {
            let dmg = SufferDamage { amount : vec![(amount, from_player)] };
            store.insert(victim, dmg).expect("Unable to insert damage");
        }
    }
}
}

我们添加了 from_player。 如果伤害来自玩家 - 那么我们会将其标记为来自玩家。 我们实际上并不关心其他实体升级,所以目前这足以区分。 现在,当某些地方创建 SufferDamage 组件时,会出现一些编译器错误; 在大多数情况下,你可以通过在创建时添加 from_player: false 来修复它们。 对于 hunger_system.rstrigger_system.rs 来说是这样。 inventory_system.rs 需要使用 from_player : true - 因为现在只有玩家可以使用物品。 melee_combat_system.rs 需要做更多的工作,以确保你不会从其他生物互相残杀中获得 XP(谢谢,狼群!)。

首先,我们需要将玩家实体添加到系统请求访问的资源列表中:

#![allow(unused)]
fn main() {
...
ReadStorage<'a, NaturalAttackDefense>,
ReadExpect<'a, Entity>
);

    fn run(&mut self, data : Self::SystemData) {
        let (entities, mut log, mut wants_melee, names, attributes, skills, mut inflict_damage,
            mut particle_builder, positions, hunger_clock, pools, mut rng,
            equipped_items, meleeweapons, wearables, natural, player_entity) = data;
...
}

然后,我们将 from_player 设置为取决于攻击实体是否与玩家匹配(一直到第 105 行):

#![allow(unused)]
fn main() {
SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage, from_player: entity == *player_entity);
}

这样就解决了知道伤害 来自哪里 的问题。 现在我们可以修改 damage_system.rs 来实际奖励 XP。 这是更新后的系统:

#![allow(unused)]
fn main() {
use specs::prelude::*;
use super::{Pools, SufferDamage, Player, Name, gamelog::GameLog, RunState, Position, Map,
    InBackpack, Equipped, LootTable, Attributes};
use crate::gamesystem::{player_hp_at_level, mana_at_level};

pub struct DamageSystem {}

impl<'a> System<'a> for DamageSystem {
    #[allow(clippy::type_complexity)]
    type SystemData = ( WriteStorage<'a, Pools>,
                        WriteStorage<'a, SufferDamage>,
                        ReadStorage<'a, Position>,
                        WriteExpect<'a, Map>,
                        Entities<'a>,
                        ReadExpect<'a, Entity>,
                        ReadStorage<'a, Attributes>
                         );

    fn run(&mut self, data : Self::SystemData) {
        let (mut stats, mut damage, positions, mut map, entities, player, attributes) = data;
        let mut xp_gain = 0;

        for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() {
            for dmg in damage.amount.iter() {
                stats.hit_points.current -= dmg.0;
                let pos = positions.get(entity);
                if let Some(pos) = pos {
                    let idx = map.xy_idx(pos.x, pos.y);
                    map.bloodstains.insert(idx);
                }

                if stats.hit_points.current < 1 && dmg.1 {
                    xp_gain += stats.level * 100;
                }
            }
        }

        if xp_gain != 0 {
            let mut player_stats = stats.get_mut(*player).unwrap();
            let player_attributes = attributes.get(*player).unwrap();
            player_stats.xp += xp_gain;
            if player_stats.xp >= player_stats.level * 1000 {
                // We've gone up a level!
                // 我们升级了!
                player_stats.level += 1;
                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;
            }
        }

        damage.clear();
    }
}
}

因此,当我们处理伤害时,如果伤害是 来自 玩家并且杀死了目标 - 我们会将经验值添加到变量 xp_gain 中。 在我们完成击杀之后,我们会检查 xp_gain 是否为非零; 如果是,我们会获取有关玩家的信息并授予他们 XP。 如果他们升级了,我们会重新计算他们的生命值和法力值。

你现在可以 cargo run,如果你杀死 10 个野兽,你将升到 2 级!

Screenshot

让升级更具戏剧性

升级是一件大事 - 你升级了,治愈了自己,并准备好在一个全新的层次上面对世界! 我们应该 让它看起来像一件大事! 我们应该做的第一件事是在游戏日志中宣布升级。 在我们之前的升级代码中,我们可以添加:

#![allow(unused)]
fn main() {
WriteExpect<'a, GameLog>
                         );

    fn run(&mut self, data : Self::SystemData) {
        let (mut stats, mut damage, positions, mut map, entities, player, attributes, mut log) = data;
...
log.entries.push(format!("Congratulations, you are now level {}", player_stats.level));
}

现在至少我们 告诉 了玩家,而不是仅仅希望他们注意到。 这仍然不算是一个盛大的庆祝,所以让我们添加一些粒子效果!

我们首先添加两个更多的数据访问器:

#![allow(unused)]
fn main() {
WriteExpect<'a, ParticleBuilder>,
ReadExpect<'a, Position>
);

    fn run(&mut self, data : Self::SystemData) {
        let (mut stats, mut damage, positions, mut map, entities, player, attributes,
            mut log, mut particles, player_pos) = data;
}

我们将在玩家上方添加一道金光!

#![allow(unused)]
fn main() {
for i in 0..10 {
    if player_pos.y - i > 1 {
        particles.request(
            player_pos.x,
            player_pos.y - i,
            rltk::RGB::named(rltk::GOLD),
            rltk::RGB::named(rltk::BLACK),
            rltk::to_cp437('░'), 200.0
        );
    }
}
}

Screenshot

技能呢?

我们现在并没有真正 使用 技能,除了给玩家很多属性 +1 之外。 所以在我们开始使用它们之前,我们将保持这部分空白。

总结

所以现在你可以升级了! 欢呼!

本章的源代码可以在这里找到

使用 web assembly 在您的浏览器中运行本章的示例(需要 WebGL2)

版权 (C) 2019, Herbert Wolverson.