改进的日志记录和计数成就


关于本教程

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

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

动手 Rust


大多数 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 构建一个 RLTK TextBuilder 对象,这是一种构建大量文本以进行渲染的安全方法,同时考虑了诸如换行之类的事情。它接受 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,你将看到一个以彩色显示的日志条目:

c71-s1.jpg

强制执行 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 你的游戏 - 并看到一个色彩鲜艳的日志:

c71-s2.jpg

让常见的日志记录任务更轻松

在遍历代码,更新日志条目时 - 出现了很多共同之处。最好强制执行一些样式一致性(并减少所需的输入量)。我们将在日志构建器(在 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,你的回合数将被计数。这是我尝试被杀死的一次运行的结果:

c71-s3.jpg

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.rsgame_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});
                }
            }
        }
    }
}
}

现在死亡会显示你在整个运行过程中遭受了多少伤害:

c71-s4.jpg

当然,你可以根据自己的意愿扩展这个功能。现在几乎所有可量化的东西都可以被追踪,如果你愿意的话。

保存和加载计数器

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。