经验与升级
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
到目前为止,我们已经深入地下城,并且实际上只通过找到更好的装备来提升了自己。 属性更好的剑、盾牌和盔甲 - 使我们有机会在更强大的敌人面前生存下来。 这很好,但这通常只是 Roguelike 和 RPG 中常见等式的一半; 击败敌人通常会奖励 经验值 - 这些经验值可以用来提升你的角色。
我们正在制作的游戏类型暗示了一些指导原则:
- 由于永久死亡的设定,你可以预料到会 死很多次。 因此,管理你的角色进度需要 简单 - 这样你就不会花费大量时间在上面,结果却不得不重新再来一次。
- 垂直进度 是一件好事:随着你深入地下城,你会变得更强大(这允许我们制作更强大的怪物)。 水平 进度在很大程度上破坏了永久死亡的意义; 如果你在游戏之间保留收益,那么 Roguelike 游戏中“每次游戏都是独一无二的”这一方面就会受到损害,你可以预料到 Reddit 上的
/r/roguelikes
的伙伴们会抱怨!
获取经验值
当你击败某些东西时,你应该获得经验值(XP)。 我们现在采用一个简单的升级方案:每次你击败某些东西时,你都会获得 100 XP * 敌人等级
。 这对于杀死强大的敌人来说收益更大 - 而对于猎杀那些你已经超越等级的生物来说,收益则相对较小。 此外,我们决定你需要 1,000 XP * 当前等级
才能升到下一级。
我们的 Pools
组件中已经有了 level
和 xp
(你几乎会认为我们正在计划这一章!)。 让我们从修改我们的 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)); }
这会在屏幕上为我们当前的等级进度添加一个金色的进度条,并显示我们当前的等级:
现在我们应该支持实际 获得 经验值。 我们应该从跟踪伤害 来自哪里 开始。 打开 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.rs
、trigger_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 级!
让升级更具戏剧性
升级是一件大事 - 你升级了,治愈了自己,并准备好在一个全新的层次上面对世界! 我们应该 让它看起来像一件大事! 我们应该做的第一件事是在游戏日志中宣布升级。 在我们之前的升级代码中,我们可以添加:
#![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 ); } } }
技能呢?
我们现在并没有真正 使用 技能,除了给玩家很多属性 +1
之外。 所以在我们开始使用它们之前,我们将保持这部分空白。
总结
所以现在你可以升级了! 欢呼!
本章的源代码可以在这里找到
使用 web assembly 在您的浏览器中运行本章的示例(需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.