改进的日志记录和计数成就
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。我希望您会喜欢本教程,并制作出很棒的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
大多数 Roguelike 游戏都很重视游戏日志。它会在最后被汇总到 morgue 文件 中(详细描述你的游戏过程),它被用来展示世界中正在发生的事情,并且对于硬核玩家来说非常宝贵。我们一直在使用一个相当简单的日志记录设置(感谢 Mark McCaskey 的辛勤工作,它不再慢得令人发指)。在本章中,我们将构建一个良好的日志记录系统 - 并将其用作成就和进度跟踪系统的基础。我们还将使日志记录 GUI 更好一些。
目前,我们通过直接调用数据结构来添加到游戏日志。它看起来像这样:
#![allow(unused)] fn main() { log.entries.push(format!("{} hits {}, for {} hp.", &name.name, &target_name.name, damage)); }
这不是一个好方法:它要求你能够直接访问日志,不提供任何格式化,并且要求系统了解日志的内部工作方式。我们也没有将日志序列化为游戏保存的一部分(以及在加载时反序列化)。最后,有很多我们本可以记录但没有记录的事情;那是因为将日志作为资源包含进来非常麻烦。像效果系统一样,它应该是无缝的、容易的并且是线程安全的(如果你不使用 WASM!)。
本章将纠正这些缺陷。
构建 API
我们将从创建一个新目录 src/gamelog
开始。我们将把 src/gamelog.rs
的内容移入其中,并将文件重命名为 mod.rs
- 换句话说,我们创建了一个新模块。这应该继续有效 - 模块的名称没有改变。
将以下内容追加到 mod.rs
:
#![allow(unused)] fn main() { pub struct LogFragment { pub color : RGB, pub text : String } }
新的 LogFragment
类型将存储日志条目的 片段。每个片段可以有一些文本和颜色,从而允许使用丰富多彩的日志条目。它们组合在一起可以构成一个日志行。
接下来,我们将创建另一个新文件 - 这次命名为 src/gamelog/logstore.rs
。将以下内容粘贴到其中:
#![allow(unused)] fn main() { use std::sync::Mutex; use super::LogFragment; use rltk::prelude::*; lazy_static! { static ref LOG : Mutex<Vec<Vec<LogFragment>>> = Mutex::new(Vec::new()); } pub fn append_fragment(fragment : LogFragment) { LOG.lock().unwrap().push(vec![fragment]); } pub fn append_entry(fragments : Vec<LogFragment>) { LOG.lock().unwrap().push(fragments); } pub fn clear_log() { LOG.lock().unwrap().clear(); } pub fn log_display() -> TextBuilder { let mut buf = TextBuilder::empty(); LOG.lock().unwrap().iter().rev().take(12).for_each(|log| { log.iter().for_each(|frag| { buf.fg(frag.color); buf.line_wrap(&frag.text); }); buf.ln(); }); buf } }
这里有很多内容需要消化:
- 核心在于,我们使用
lazy_static
来定义一个 全局 日志条目存储。它是一个向量的向量,这次构成了片段。因此,外部向量是日志中的 行,内部向量构成了组成日志的 片段。它受到Mutex
的保护,使其在线程环境中可以安全使用。 append_fragment
锁定日志,并将单个片段作为新行追加。append_entry
锁定日志,并追加一个片段向量(新行)。clear_log
的作用正如其名称所示:它清空日志。log_display
构建一个 RLTKTextBuilder
对象,这是一种构建大量文本以进行渲染的安全方法,同时考虑了诸如换行之类的事情。它接受 12 个条目,因为这是我们可以显示的最大日志量。
在 mod.rs
中,添加以下三行来处理模块的使用和导出部分内容:
#![allow(unused)] fn main() { mod logstore; use logstore::*; pub use logstore::{clear_log, log_display}; }
这使我们可以大大简化日志的显示。打开 gui.rs
,找到日志绘制代码(在示例中是第 248 行)。将日志绘制替换为:
#![allow(unused)] fn main() { // Draw the log let mut block = TextBlock::new(1, 46, 79, 58); block.print(&gamelog::log_display()); block.render(&mut rltk::BACKEND_INTERNAL.lock().consoles[0].console); }
这指定了日志文本块的确切位置,作为一个 RLTK TextBlock
对象。然后它将 log_display()
的结果打印到块中,并将其渲染到控制台零(我们正在使用的控制台)。
现在,我们需要一种向日志添加文本的方法。构建器模式是一个自然的选择;大多数时候,我们都在逐步构建日志条目中的细节。创建另一个文件 src/gamelog/builder.rs
:
#![allow(unused)] fn main() { use rltk::prelude::*; use super::{LogFragment, append_entry}; pub struct Logger { current_color : RGB, fragments : Vec<LogFragment> } impl Logger { pub fn new() -> Self { Logger{ current_color : RGB::named(rltk::WHITE), fragments : Vec::new() } } pub fn color(mut self, color: (u8, u8, u8)) -> Self { self.current_color = RGB::named(color); self } pub fn append<T: ToString>(mut self, text : T) -> Self { self.fragments.push( LogFragment{ color : self.current_color, text : text.to_string() } ); self } pub fn log(self) { append_entry(self.fragments) } } }
这定义了一个新类型 Logger
。它跟踪当前的输出颜色,以及组成日志条目的当前片段列表。new
函数创建一个新的 Logger,而 log
将其提交给受互斥锁保护的全局变量。你可以调用 color
来更改当前的写入颜色,并调用 append
来添加字符串(我们正在使用 ToString
,所以不再需要在到处进行混乱的 to_string()
调用!)。
在 gamelog/mod.rs
中,我们想要使用并导出这个模块:
#![allow(unused)] fn main() { mod builder; pub use builder::*; }
为了查看它的实际效果,打开 main.rs
并找到我们向资源列表添加新日志文件的行,以及 “Welcome to Rusty Roguelike” 行。现在,我们将保留原始的 - 并利用新的设置来启动日志:
#![allow(unused)] fn main() { gs.ecs.insert(gamelog::GameLog{ entries : vec!["Welcome to Rusty Roguelike".to_string()] }); gamelog::clear_log(); gamelog::Logger::new() .append("Welcome to") .color(rltk::CYAN) .append("Rusty Roguelike") .log(); }
这很简洁明了:无需获取资源,并且文本/颜色追加易于阅读!如果你现在 cargo run
,你将看到一个以彩色显示的日志条目:
强制执行 API 使用
现在是时候破坏一些东西了。在 src/gamelog/mod.rs
中,删除 以下内容:
#![allow(unused)] fn main() { pub struct GameLog { pub entries : Vec<String> } }
如果你正在使用 IDE,你的项目刚刚变成了一片红色!我们刚刚删除了旧的日志记录方式 - 因此每个对旧日志的引用现在都变成了编译失败。没关系,因为我们想要过渡到新系统。
从 main.rs
开始,我们可以删除对旧日志的引用。删除新的日志行,以及我们之前添加的所有日志记录信息。找到 generate_world_map
函数,并将初始的日志清除/设置移到那里:
#![allow(unused)] fn main() { fn generate_world_map(&mut self, new_depth : i32, offset: i32) { self.mapgen_index = 0; self.mapgen_timer = 0.0; self.mapgen_history.clear(); let map_building_info = map::level_transition(&mut self.ecs, new_depth, offset); if let Some(history) = map_building_info { self.mapgen_history = history; } else { map::thaw_level_entities(&mut self.ecs); } gamelog::clear_log(); gamelog::Logger::new() .append("Welcome to") .color(rltk::CYAN) .append("Rusty Roguelike") .log(); } }
如果你现在 cargo build
项目,你将会有很多错误。我们需要逐步处理并更新所有日志记录引用,以使用新系统。
使用 API
打开 src/inventory_system/collection_system.rs
。在 use
语句中,删除对 gamelog::GameLog
的引用(它不再存在了)。删除寻找游戏日志的 WriteExpect
(以及元组中匹配的 mut gamelog
)。将 gamelog.push
语句替换为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .append("You pick up the") .color(rltk::CYAN) .append( super::obfuscate_name(pickup.item, &names, &magic_items, &obfuscated_names, &dm) ) .log(); }
你需要在 src/inventory_system/drop_system.rs
中进行基本相同的更改。在删除导入和资源后,日志消息系统变为:
#![allow(unused)] fn main() { if entity == *player_entity { crate::gamelog::Logger::new() .append("You drop the") .color(rltk::CYAN) .append( super::obfuscate_name(to_drop.item, &names, &magic_items, &obfuscated_names, &dm) ) .log(); } }
同样,在 src/inventory_system/equip_use.rs
中,删除 gamelog
。还要删除 log_entries
变量和追加它的循环。有很多日志条目需要清理:
#![allow(unused)] fn main() { // Cursed item unequipping crate::gamelog::Logger::new() .append("You cannot unequip") .color(rltk::CYAN) .append(&name.name) .color(rltk::WHITE) .append("- it is cursed!") .log(); can_equip = false; ... // Unequipped item crate::gamelog::Logger::new() .append("You unequip") .color(rltk::CYAN) .append(&name.name) .log(); ... // Wield crate::gamelog::Logger::new() .append("You equip") .color(rltk::CYAN) .append(&names.get(useitem.item).unwrap().name) .log(); }
同样,文件 src/hunger_system.rs
需要更新。再次删除 gamelog
并将 log.push
行替换为使用新系统的等效行。
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::ORANGE) .append("You are no longer well fed") .log(); ... crate::gamelog::Logger::new() .color(rltk::ORANGE) .append("You are hungry") .log(); ... crate::gamelog::Logger::new() .color(rltk::RED) .append("You are starving!") .log(); ... crate::gamelog::Logger::new() .color(rltk::RED) .append("Your hunger pangs are getting painful! You suffer 1 hp damage.") .log(); }
src/trigger_system.rs
也需要相同的处理。再次删除 gamelog
并替换日志条目。我们将使用一些颜色高亮来强调陷阱:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::RED) .append(&name.name) .color(rltk::WHITE) .append("triggers!") .log(); }
src/ai/quipping.rs
需要完全相同的处理。删除 gamelog
,并将日志调用替换为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::YELLOW) .append(&name.name) .color(rltk::WHITE) .append("says") .color(rltk::CYAN) .append(&quip.available[quip_index]) .log(); }
src/ai/encumbrance_system.rs
也有相同的更改。再次,gamelog
必须消失 - 并且日志追加被替换为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::ORANGE) .append("You are overburdened, and suffering an initiative penalty.") .log(); }
src/effects/damage.rs
的日志记录方式略有不同,但我们现在可以统一机制。首先删除 use crate::gamelog::GameLog;
行。然后将所有 log_entries.push
行替换为使用新的 Logger
接口的行:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::MAGENTA) .append("Congratulations, you are now level") .append(format!("{}", player_stats.level)) .log(); ... crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel stronger!").log(); ... crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel healthier!").log(); ... crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel quicker!").log(); ... crate::gamelog::Logger::new().color(rltk::GREEN).append("You feel smarter!").log(); }
src\effects\trigger.rs
中的情况也相同;删除 GameLog
并将日志代码替换为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::CYAN) .append(&ecs.read_storage::<Name>().get(item).unwrap().name) .color(rltk::WHITE) .append("is out of charges!") .log(); ... crate::gamelog::Logger::new() .append("You eat the") .color(rltk::CYAN) .append(&names.get(entity).unwrap().name) .log(); ... crate::gamelog::Logger::new().append("The map is revealed to you!").log(); ... crate::gamelog::Logger::new().append("You are already in town, so the scroll does nothing.").log(); ... crate::gamelog::Logger::new().append("You are telported back to town!").log(); ... }
再次,src/player.rs
也是类似的情况。删除 GameLog
,并将日志条目替换为新的构建器语法:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .append("You fire at") .color(rltk::CYAN) .append(&name.name) .log(); ... crate::gamelog::Logger::new().append("There is no way down from here.").log(); ... crate::gamelog::Logger::new().append("There is no way up from here.").log(); ... None => crate::gamelog::Logger::new().append("There is nothing here to pick up.").log(), ... crate::gamelog::Logger::new().append("You don't have enough mana to cast that!").log(); }
visibility_system.rs
中的情况也相同。再次删除 GameLog
并将日志推送替换为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .append("You spotted:") .color(rltk::RED) .append(&name.name) .log(); }
再次,melee_combat_system.rs
需要相同的更改:不再有 GameLog
,并将文本输出更新为使用新的构建系统:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::YELLOW) .append(&name.name) .color(rltk::WHITE) .append("hits") .color(rltk::YELLOW) .append(&target_name.name) .color(rltk::WHITE) .append("for") .color(rltk::RED) .append(format!("{}", damage)) .color(rltk::WHITE) .append("hp.") .log(); ... crate::gamelog::Logger::new() .color(rltk::CYAN) .append(&name.name) .color(rltk::WHITE) .append("considers attacking") .color(rltk::CYAN) .append(&target_name.name) .color(rltk::WHITE) .append("but misjudges the timing!") .log(); ... crate::gamelog::Logger::new() .color(rltk::CYAN) .append(&name.name) .color(rltk::WHITE) .append("attacks") .color(rltk::CYAN) .append(&target_name.name) .color(rltk::WHITE) .append("but can't connect.") .log(); }
现在你应该对所需的更改有一个很好的理解了。如果你查看 源代码,我已经对所有其他 gamelog
实例进行了更改。
在你完成所有更改后,你可以 cargo run
你的游戏 - 并看到一个色彩鲜艳的日志:
让常见的日志记录任务更轻松
在遍历代码,更新日志条目时 - 出现了很多共同之处。最好强制执行一些样式一致性(并减少所需的输入量)。我们将在日志构建器(在 src/gamelog/builder.rs
中)中添加一些方法来帮助我们:
#![allow(unused)] fn main() { pub fn npc_name<T: ToString>(mut self, text : T) -> Self { self.fragments.push( LogFragment{ color : RGB::named(rltk::YELLOW), text : text.to_string() } ); self } pub fn item_name<T: ToString>(mut self, text : T) -> Self { self.fragments.push( LogFragment{ color : RGB::named(rltk::CYAN), text : text.to_string() } ); self } pub fn damage(mut self, damage: i32) -> Self { self.fragments.push( LogFragment{ color : RGB::named(rltk::RED), text : format!("{}", damage).to_string() } ); self } }
现在我们可以再次遍历并更新一些日志条目代码,使用更简单的语法。例如,在 src\ai\quipping.rs
中,我们可以替换:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .color(rltk::YELLOW) .append(&name.name) .color(rltk::WHITE) .append("says") .color(rltk::CYAN) .append(&quip.available[quip_index]) .log(); }
为:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .npc_name(&name.name) .append("says") .npc_name(&quip.available[quip_index]) .log(); }
或者在 melee_combat_system.rs
中,可以大大缩短伤害公告:
#![allow(unused)] fn main() { crate::gamelog::Logger::new() .npc_name(&name.name) .append("hits") .npc_name(&target_name.name) .append("for") .damage(damage) .append("hp.") .log(); }
再次,我已经遍历了项目源代码并应用了这些增强功能。
保存和加载日志
为了更轻松地保存和加载日志,我们将在 gamelog/logstore.rs
中添加两个辅助函数:
#![allow(unused)] fn main() { pub fn clone_log() -> Vec<Vec<crate::gamelog::LogFragment>> { LOG.lock().unwrap().clone() } pub fn restore_log(log : &mut Vec<Vec<crate::gamelog::LogFragment>>) { LOG.lock().unwrap().clear(); LOG.lock().unwrap().append(log); } }
第一个函数提供日志的克隆副本。第二个函数清空日志,并追加一个新的日志。你需要打开 gamelog/mod.rs
并将它们添加到导出的函数列表中:
#![allow(unused)] fn main() { pub use logstore::{clear_log, log_display, clone_log, restore_log}; }
当你在编辑 mod.rs
时,我们需要向 LogFragment
结构添加一些派生:
#![allow(unused)] fn main() { use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize, Clone)] pub struct LogFragment { pub color : RGB, pub text : String } }
现在打开 components.rs
,并修改 DMSerializationHelper
结构以包含日志:
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct DMSerializationHelper { pub map : super::map::MasterDungeonMap, pub log : Vec<Vec<crate::gamelog::LogFragment>> } }
打开 saveload_system.rs
,我们将在序列化地图时包含日志:
#![allow(unused)] fn main() { let savehelper2 = ecs .create_entity() .with(DMSerializationHelper{ map : dungeon_master, log: crate::gamelog::clone_log() }) .marked::<SimpleMarker<SerializeMe>>() .build(); }
当我们反序列化地图时,我们也将恢复日志:
#![allow(unused)] fn main() { for (e,h) in (&entities, &helper2).join() { let mut dungeonmaster = ecs.write_resource::<super::map::MasterDungeonMap>(); *dungeonmaster = h.map.clone(); deleteme2 = Some(e); crate::gamelog::restore_log(&mut h.log.clone()); } }
这就是保存/加载日志的全部内容:它与 Serde 配合良好(在完整的 JSON 上可能会有点慢),但它工作得很好。
计数事件
作为迈向成就的第一步,我们需要能够计数相关事件。创建一个新文件 src/gamelog/events.rs
,并将以下内容粘贴到其中:
#![allow(unused)] fn main() { use std::collections::HashMap; use std::sync::Mutex; lazy_static! { static ref EVENTS : Mutex<HashMap<String, i32>> = Mutex::new(HashMap::new()); } pub fn clear_events() { EVENTS.lock().unwrap().clear(); } pub fn record_event<T: ToString>(event: T, n : i32) { let event_name = event.to_string(); let mut events_lock = EVENTS.lock(); let mut events = events_lock.as_mut().unwrap(); if let Some(e) = events.get_mut(&event_name) { *e += n; } else { events.insert(event_name, n); } } pub fn get_event_count<T: ToString>(event: T) -> i32 { let event_name = event.to_string(); let events_lock = EVENTS.lock(); let events = events_lock.unwrap(); if let Some(e) = events.get(&event_name) { *e } else { 0 } } }
这与我们存储日志的方式类似:它是一个 “lazy static”,带有互斥锁安全包装器。内部是一个 HashMap
,以事件名称为索引,并包含一个计数器。record_event
将一个事件添加到运行总计中(如果不存在则创建一个新事件)。get_event_count
返回 0,或指定名称计数器的总数。
在 main.rs
中,找到 RunState::AwaitingInput
的主循环处理程序 - 我们将扩展它来计算玩家存活的回合数:
#![allow(unused)] fn main() { RunState::AwaitingInput => { newrunstate = player_input(self, ctx); if newrunstate != RunState::AwaitingInput { crate::gamelog::record_event("Turn", 1); } } }
我们还应该在 generate_world_map
的末尾清除计数器状态:
#![allow(unused)] fn main() { fn generate_world_map(&mut self, new_depth : i32, offset: i32) { self.mapgen_index = 0; self.mapgen_timer = 0.0; self.mapgen_history.clear(); let map_building_info = map::level_transition(&mut self.ecs, new_depth, offset); if let Some(history) = map_building_info { self.mapgen_history = history; } else { map::thaw_level_entities(&mut self.ecs); } gamelog::clear_log(); gamelog::Logger::new() .append("Welcome to") .color(rltk::CYAN) .append("Rusty Roguelike") .log(); gamelog::clear_events(); } }
为了演示它是否有效,让我们在死亡屏幕上显示玩家存活的回合数。在 gui.rs
中,打开函数 game_over
并添加一个回合计数器:
#![allow(unused)] fn main() { 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(19, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), &format!("You lived for {} turns.", crate::gamelog::get_event_count("Turn"))); ctx.print_color_centered(21, 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 } } }
如果你现在 cargo run
,你的回合数将被计数。这是我尝试被杀死的一次运行的结果:
Bracket 进行数量调查
这是一个非常灵活的系统:你几乎可以从任何地方计数任何你喜欢的东西!让我们记录玩家在整个游戏中受到的伤害量。打开 src/effects/damage.rs
并修改函数 inflict_damage
:
#![allow(unused)] fn main() { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); let player_entity = ecs.fetch::<Entity>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let Some(creator) = damage.creator { if creator == target { return; } } if let EffectType::Damage{amount} = damage.effect_type { pool.hit_points.current -= amount; add_effect(None, EffectType::Bloodstain, Targets::Single{target}); add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg : rltk::RGB::named(rltk::ORANGE), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, Targets::Single{target} ); if target == *player_entity { crate::gamelog::record_event("Damage Taken", amount); } if damage.creator == *player_entity { crate::gamelog::record_event("Damage Inflicted", amount); } if pool.hit_points.current < 1 { add_effect(damage.creator, EffectType::EntityDeath, Targets::Single{target}); } } } } } }
我们将再次修改 gui.rs
的 game_over
函数以显示受到的伤害:
#![allow(unused)] fn main() { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); let player_entity = ecs.fetch::<Entity>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let Some(creator) = damage.creator { if creator == target { return; } } if let EffectType::Damage{amount} = damage.effect_type { pool.hit_points.current -= amount; add_effect(None, EffectType::Bloodstain, Targets::Single{target}); add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('‼'), fg : rltk::RGB::named(rltk::ORANGE), bg : rltk::RGB::named(rltk::BLACK), lifespan: 200.0 }, Targets::Single{target} ); if target == *player_entity { crate::gamelog::record_event("Damage Taken", amount); } if let Some(creator) = damage.creator { if creator == *player_entity { crate::gamelog::record_event("Damage Inflicted", amount); } } if pool.hit_points.current < 1 { add_effect(damage.creator, EffectType::EntityDeath, Targets::Single{target}); } } } } } }
现在死亡会显示你在整个运行过程中遭受了多少伤害:
当然,你可以根据自己的意愿扩展这个功能。现在几乎所有可量化的东西都可以被追踪,如果你愿意的话。
保存和加载计数器
向 src/gamelog/events.rs
添加另外两个函数:
#![allow(unused)] fn main() { pub fn clone_events() -> HashMap<String, i32> { EVENTS.lock().unwrap().clone() } pub fn load_events(events : HashMap<String, i32>) { EVENTS.lock().unwrap().clear(); events.iter().for_each(|(k,v)| { EVENTS.lock().unwrap().insert(k.to_string(), *v); }); } }
现在打开 components.rs
,并修改 DMSerializationHelper
:
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct DMSerializationHelper { pub map : super::map::MasterDungeonMap, pub log : Vec<Vec<crate::gamelog::LogFragment>>, pub events : HashMap<String, i32> } }
然后在 saveload_system.rs
中,我们可以将克隆的事件包含在我们的序列化中:
#![allow(unused)] fn main() { let savehelper2 = ecs .create_entity() .with(DMSerializationHelper{ map : dungeon_master, log: crate::gamelog::clone_log(), events : crate::gamelog::clone_events() }) .marked::<SimpleMarker<SerializeMe>>() .build(); }
并在我们反序列化时导入事件:
#![allow(unused)] fn main() { for (e,h) in (&entities, &helper2).join() { let mut dungeonmaster = ecs.write_resource::<super::map::MasterDungeonMap>(); *dungeonmaster = h.map.clone(); deleteme2 = Some(e); crate::gamelog::restore_log(&mut h.log.clone()); crate::gamelog::load_events(h.events.clone()); } }
总结
我们现在有了色彩鲜艳的日志,以及玩家成就的计数器。这使我们离 Steam(或 XBOX)风格的成就仅一步之遥 - 我们将在接下来的章节中介绍。
本章的源代码可以在 这里 找到
在你的浏览器中使用 Web 程序集运行本章的示例 (需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。