数据驱动设计:Raw Files
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出伟大的游戏!
如果您喜欢这个教程并希望我继续创作,请考虑支持我的 Patreon。
如果您玩过 Dwarf Fortress,其(底层)一个显著的特点就是 raw file 系统。游戏中大量的内容都在 raws
中详细说明,您可以完全“mod”(修改)游戏,使其变成其他的东西。其他游戏,例如 Tome 4,更进一步,为 所有内容 定义了脚本引擎文件 - 您可以随心所欲地自定义游戏。一旦实现,raws
将您的游戏变成更像一个 引擎 - 显示/管理与 raw files 中编写的内容的交互。这并不是说引擎很简单:它必须支持 raw files 中指定的所有内容!
这被称为 数据驱动设计:您的游戏更多地是由描述它的数据定义的,而不是实际的引擎机制。它有以下几个优点:
- 它使得进行更改非常容易;您不必每次想更改哥布林,或者制作一个新的变种(例如“胆小的哥布林”)时都去挖掘
spawner.rs
。相反,您编辑raws
以包含您的新怪物,将其添加到生成、战利品和阵营表格中,然后这个怪物就出现在您的游戏中了!(除非 “胆小” 实际上需要新的支持代码 - 在这种情况下,您也需要编写它)。 - 数据驱动设计与实体组件系统 (ECS) 美妙地结合在一起。
raws
充当一个 模板,您可以通过组合组件来构建实体,直到它与您的raw
描述相匹配。 - 数据驱动设计使人们可以轻松更改您创建的游戏。对于像这样的教程来说,这一点非常重要:我更希望您从本教程中走出来后能够制作自己的游戏,而不仅仅是重新制作这个游戏!
WebAssembly 的一个缺点
WebAssembly 不容易从您的计算机读取文件。这就是为什么我们开始使用 嵌入 系统来处理资源;否则您必须制作一堆钩子,通过 JavaScript 调用来读取游戏数据,以下载资源,将其作为数据数组获取,并将数组传递到 WebAssembly 模块中。可能还有比嵌入所有内容更好的方法,但在我找到一个好的方法(并且也能在原生代码中工作)之前,我们将坚持使用嵌入。
这消除数据驱动设计的一个优势:您仍然需要重新编译游戏。因此我们将使嵌入成为可选的;如果我们 可以 从磁盘读取文件,我们将这样做。在实践中,这意味着当您发布游戏时,您必须包含可执行文件 和 raw files - 或者将它们嵌入到最终构建中。
确定 Raw files 的格式
在一些项目中,我使用脚本语言 Lua
来处理这类事情。Lua 是一种很棒的语言,并且拥有可执行的配置出奇地有用(配置可以包含函数和助手函数来构建自身)。但这对于本项目来说有点过度了。我们已经在游戏的保存/加载中支持 JSON,因此我们也将使用它来处理 Raws
。
查看当前游戏中的 spawner.rs
应该会给我们一些关于在这些文件中放入什么内容的线索。感谢我们对组件的使用,已经有很多共享功能可以构建。例如,治疗药水 的定义如下所示:
#![allow(unused)] fn main() { fn health_potion(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('¡'), fg: RGB::named(rltk::MAGENTA), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Health Potion".to_string() }) .with(Item{}) .with(Consumable{}) .with(ProvidesHealing{ heal_amount: 8 }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
在 JSON 中,我们可能会选择像这样的表示形式(只是一个例子):
{
"name" : "Healing Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000"
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
}
}
创建 raw files
您的包应按如下方式布局:
| 根文件夹
\ - src (您的源文件)
在根级别,我们将创建一个名为 raws
的新目录/文件夹。因此您的目录树应如下所示:
| 根文件夹
\ - src (您的源文件)
\ - raws
在此目录中,创建一个新文件:spawns.json
。我们将暂时将所有定义放在一个文件中;这稍后会更改,但我们希望获得对我们的数据驱动野心的引导支持。在此文件中,我们将放入一些我们当前在 spawner.rs
中支持的实体的定义。我们将从几个物品开始:
{
{
"items" : [
{
"name" : "Health Potion",
"renderable": {
"glyph" : "!",
"fg" : "#FF00FF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : { "provides_healing" : "8" }
}
},
{
"name" : "Magic Missile Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20"
}
}
}
]
}
如果您不熟悉 JSON 格式,它基本上是数据的 JavaScript 转储:
- 我们用
{
和}
包裹文件以表示我们要加载的 对象。这将最终成为我们的Raws
对象。 - 然后我们有一个名为
Items
的 数组 - 它将保存我们的物品。 - 每个
Item
都有一个name
- 这直接映射到Name
组件。 - 物品可能有一个
renderable
结构,列出字形、前景色和背景色。 - 这些物品是
consumable
(消耗品),我们在一个 “键/值映射” 中列出它们的效果 - 基本上是一个HashMap
,就像我们以前使用过的那样,在其他语言中是一个Dictionary
。
最终我们将向 spawns 列表添加更多内容,但让我们首先让这些内容起作用。
嵌入 Raw Files
在您的项目 src
目录中,创建一个新目录:src/raws
。我们可以合理地预期这个模块会变得很大,因此我们将从一开始就支持将其分解成更小的部分。为了符合 Rust 构建模块的要求,在新文件夹中创建一个名为 mod.rs
的新文件:
#![allow(unused)] fn main() { rltk::embedded_resource!(RAW_FILE, "../../raws/spawns.json"); pub fn load_raws() { rltk::link_resource!(RAW_FILE, "../../raws/spawns.json"); } }
并在 main.rs
的顶部将其添加到我们使用的模块列表中:
#![allow(unused)] fn main() { pub mod raws; }
在我们的初始化中,在组件初始化之后,在您开始添加到 World
之前,添加对 load_raws
的调用:
#![allow(unused)] fn main() { ... gs.ecs.register::<Door>(); gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new()); raws::load_raws(); gs.ecs.insert(Map::new(1, 64, 64)); ... }
spawns.json
文件现在将被嵌入到您的可执行文件中,这要归功于 RLTK 的嵌入系统。
解析 raw files
这是困难的部分:我们需要一种 读取 我们创建的 JSON 文件的方法,并将其转换为我们可以在 Rust 中使用的格式。回到 mod.rs
,我们可以扩展该函数以将嵌入的数据加载为字符串:
#![allow(unused)] fn main() { // 将原始数据检索为 u8(8 位无符号字符)数组 let raw_data = rltk::embedding::EMBED .lock() .unwrap() .get_resource("../../raws/spawns.json".to_string()) .unwrap(); let raw_string = std::str::from_utf8(&raw_data).expect("Unable to convert to a valid UTF-8 string."); }
如果无法找到资源,或者无法将其解析为常规字符串,这将导致 panic(崩溃)(Rust 喜欢 UTF-8 Unicode 编码,因此我们将使用它。它允许我们包含扩展字形,我们可以通过 RLTK 的 to_cp437
函数解析它们 - 因此效果很好!)。
现在我们需要实际 解析 JSON 为一些可用的东西。就像我们的 saveload.rs
系统一样,我们可以使用 Serde 来做到这一点。现在,我们只将结果转储到控制台,以便我们可以看到它 确实 做了一些事情:
#![allow(unused)] fn main() { let decoder : Raws = serde_json::from_str(&raw_string).expect("Unable to parse JSON"); rltk::console::log(format!("{:?}", decoder)); }
(看到了神秘的 {:?}
吗?这是一种打印关于结构的 调试 信息的方式)。这将编译失败,因为我们实际上还没有实现 Raws
- 它正在寻找的类型。
为了清晰起见,我们将实际处理数据的类放在它们自己的文件 raws/item_structs.rs
中。这是该文件:
#![allow(unused)] fn main() { use serde::{Deserialize}; use std::collections::HashMap; #[derive(Deserialize, Debug)] pub struct Raws { pub items : Vec<Item> } #[derive(Deserialize, Debug)] pub struct Item { pub name : String, pub renderable : Option<Renderable>, pub consumable : Option<Consumable> } #[derive(Deserialize, Debug)] pub struct Renderable { pub glyph: String, pub fg : String, pub bg : String, pub order: i32 } #[derive(Deserialize, Debug)] pub struct Consumable { pub effects : HashMap<String, String> } }
在文件顶部,请确保包含 use serde::{Deserialize};
和 use std::collections::HashMap;
以包含我们需要的类型。另请注意,我们在派生类型列表中包含了 Debug
。这允许 Rust 打印结构的调试副本,以便我们可以看到代码做了什么。另请注意,很多东西都是 Option
。这样,如果一个物品 没有 该条目,解析也能工作。稍后读取它们会稍微复杂一些,但我们可以忍受!
如果您现在 cargo run
项目,请忽略游戏窗口 - 观看控制台。您将看到以下内容:
Raws { items: [Item { name: "Healing Potion", renderable: Some(Renderable { glyph: "!", fg: "#FF00FF", bg: "#000000" }), consumable: Some(Consumable { effects: {"provides_healing": "8"} }) }, Item { name: "Magic Missile Scroll", renderable: Some(Renderable { glyph: ")", fg: "#00FFFF", bg: "#000000"
}), consumable: Some(Consumable { effects: {"damage": "20", "ranged": "6"} }) }] }
这 超级 丑陋且格式糟糕,但您可以看到它包含我们输入的数据!
存储和索引我们的 raw item 数据
拥有这些(主要是文本)数据很棒,但在它可以直接关联到生成实体之前,它并没有真正帮助我们。我们也在加载数据后立即丢弃了数据!
我们想要创建一个结构来保存我们所有的 raw 数据,并提供有用的服务,例如完全根据 raws
中的数据生成对象。我们将创建一个新文件 raws/rawmaster.rs
:
#![allow(unused)] fn main() { use std::collections::HashMap; use specs::prelude::*; use crate::components::*; use super::{Raws}; pub struct RawMaster { raws : Raws, item_index : HashMap<String, usize> } impl RawMaster { pub fn empty() -> RawMaster { RawMaster { raws : Raws{ items: Vec::new() }, item_index : HashMap::new() } } pub fn load(&mut self, raws : Raws) { self.raws = raws; self.item_index = HashMap::new(); for (i,item) in self.raws.items.iter().enumerate() { self.item_index.insert(item.name.clone(), i); } } } }
这非常直接,并且完全在我们目前学到的 Rust 知识范围内:我们创建了一个名为 RawMaster
的结构,它获得 Raws
数据的私有副本和一个 HashMap
,用于存储物品名称及其在 Raws.items
中的索引。empty
构造函数的作用正是如此:它创建了 RawMaster
结构的一个完全空的版本。load
接受反序列化的 Raws
结构,存储它,并按名称和在 items
数组中的位置索引物品。
从任何地方访问 Raw 数据
现在是 Rust 如果不使全局变量难以使用就好了的时刻之一;我们想要完全一个 RawMaster
数据的副本,并且我们希望能够从任何地方 读取 它。您 可以 通过一堆 unsafe
代码来实现这一点,但我们将成为优秀的 “Rustaceans” 并使用一种流行的方法:lazy_static
。此功能不是语言本身的一部分,因此我们需要向 cargo.toml
添加一个 crate。将以下行添加到文件中的 [dependencies]
中:
lazy_static = "1.4.0"
现在我们做一点舞蹈,以使全局变量可以从任何地方安全地访问。在 main.rs
的导入部分末尾,添加:
#![allow(unused)] fn main() { #[macro_use] extern crate lazy_static; }
这与我们为其他宏所做的事情类似:它告诉 Rust 我们想从 crate lazy_static
导入宏。在 mod.rs
中,声明以下内容:
#![allow(unused)] fn main() { mod rawmaster; pub use rawmaster::*; use std::sync::Mutex; }
还有:
#![allow(unused)] fn main() { lazy_static! { pub static ref RAWS : Mutex<RawMaster> = Mutex::new(RawMaster::empty()); } }
lazy_static!
宏为我们做了很多繁重的工作,以使其安全。有趣的部分是我们仍然必须使用 Mutex
。互斥锁是一种构造,可确保一次只有一个线程可以写入结构。您通过调用 lock
来访问互斥锁 - 它现在是您的,直到锁超出范围。因此,在我们的 load_raws
函数中,我们需要填充它:
#![allow(unused)] fn main() { // 将原始数据检索为 u8(8 位无符号字符)数组 let raw_data = rltk::embedding::EMBED .lock() .get_resource("../../raws/spawns.json".to_string()) .unwrap(); let raw_string = std::str::from_utf8(&raw_data).expect("Unable to convert to a valid UTF-8 string."); let decoder : Raws = serde_json::from_str(&raw_string).expect("Unable to parse JSON"); RAWS.lock().unwrap().load(decoder); }
您会注意到 RLTK 的 embedding
系统本身也在悄悄地使用 lazy_static
- 这就是 lock
和 unwrap
代码的用途:它管理 Mutex。因此,对于我们的 RAWS
全局变量,我们 lock
它(检索一个作用域锁),unwrap
该锁(以允许我们访问内容),并调用我们之前编写的 load
函数。相当拗口,但现在我们可以安全地共享 RAWS
数据,而无需担心线程问题。一旦加载,我们可能永远不会再写入它 - 并且当您没有大量线程运行时,用于读取的互斥锁几乎是瞬间完成的。
从 RAWS 生成物品
在 rawmaster.rs
中,我们将创建一个新函数:
#![allow(unused)] fn main() { pub fn spawn_named_item(raws: &RawMaster, new_entity : EntityBuilder, key : &str, pos : SpawnType) -> Option<Entity> { if raws.item_index.contains_key(key) { let item_template = &raws.raws.items[raws.item_index[key]]; let mut eb = new_entity; // 在指定位置生成 match pos { SpawnType::AtPosition{x,y} => { eb = eb.with(Position{ x, y }); } } // Renderable if let Some(renderable) = &item_template.renderable { eb = eb.with(crate::components::Renderable{ glyph: rltk::to_cp437(renderable.glyph.chars().next().unwrap()), fg : rltk::RGB::from_hex(&renderable.fg).expect("Invalid RGB"), bg : rltk::RGB::from_hex(&renderable.bg).expect("Invalid RGB"), render_order : renderable.order }); } eb = eb.with(Name{ name : item_template.name.clone() }); eb = eb.with(crate::components::Item{}); if let Some(consumable) = &item_template.consumable { eb = eb.with(crate::components::Consumable{}); for effect in consumable.effects.iter() { let effect_name = effect.0.as_str(); match effect_name { "provides_healing" => { eb = eb.with(ProvidesHealing{ heal_amount: effect.1.parse::<i32>().unwrap() }) } "ranged" => { eb = eb.with(Ranged{ range: effect.1.parse::<i32>().unwrap() }) }, "damage" => { eb = eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }) } _ => { rltk::console::log(format!("Warning: consumable effect {} not implemented.", effect_name)); } } } } return Some(eb.build()); } None } }
这是一个很长的函数,但它实际上非常直接 - 并且使用了我们之前多次遇到的模式。它执行以下操作:
- 它查找我们传递的
key
是否存在于item_index
中。如果不存在,则返回None
- 它什么也没做。 - 如果
key
确实存在,则它将Name
组件添加到实体 - 使用 raw 文件中的名称。 - 如果
Renderable
存在于物品定义中,它将创建一个类型为Renderable
的组件。 - 如果
Consumable
存在于物品定义中,它会创建一个新的消耗品。它迭代effect
字典中的所有键/值对,根据需要添加效果组件。
现在您可以打开 spawner.rs
并修改 spawn_entity
:
#![allow(unused)] fn main() { pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) { let map = ecs.fetch::<Map>(); let width = map.width as usize; let x = (*spawn.0 % width) as i32; let y = (*spawn.0 / width) as i32; std::mem::drop(map); let item_result = spawn_named_item(&RAWS.lock().unwrap(), ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y}); if item_result.is_some() { return; } match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y), "Orc" => orc(ecs, x, y), "Fireball Scroll" => fireball_scroll(ecs, x, y), "Confusion Scroll" => confusion_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), "Magic Mapping Scroll" => magic_mapping_scroll(ecs, x, y), "Bear Trap" => bear_trap(ecs, x, y), "Door" => door(ecs, x, y), _ => {} } } }
请注意,我们已经删除了添加到 spawns.json
中的物品。我们也可以删除关联的函数。当我们完成时,spawner.rs
将非常小!所以这里的魔力在于它调用 spawn_named_item
,使用相当丑陋的 &RAWS.lock().unwrap()
来安全访问我们的 RAWS
全局变量。如果它匹配了一个键,它将返回 Some(Entity)
- 否则,我们得到 None
。因此,我们检查 item_result.is_some()
,如果成功从数据中生成了某些东西,则返回。否则,我们使用新代码。
您还需要将 raws::*
添加到从 super
导入的物品列表中。
如果您现在 cargo run
,游戏将像以前一样运行 - 包括治疗药水和魔法飞弹卷轴。
添加其余的消耗品
我们将继续并将其余的消耗品添加到 spawns.json
中:
...
{
"name" : "Fireball Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#FFA500",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"area_of_effect" : "3"
}
}
},
{
"name" : "Confusion Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"ranged" : "6",
"damage" : "20",
"confusion" : "4"
}
}
},
{
"name" : "Magic Mapping Scroll",
"renderable": {
"glyph" : ")",
"fg" : "#AAAAFF",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"magic_mapping" : ""
}
}
},
{
"name" : "Rations",
"renderable": {
"glyph" : "%",
"fg" : "#00FF00",
"bg" : "#000000",
"order" : 2
},
"consumable" : {
"effects" : {
"food" : ""
}
}
}
]
}
我们将它们的效果放入 rawmaster.rs
的 spawn_named_item
函数中:
#![allow(unused)] fn main() { if let Some(consumable) = &item_template.consumable { eb = eb.with(crate::components::Consumable{}); for effect in consumable.effects.iter() { let effect_name = effect.0.as_str(); match effect_name { "provides_healing" => { eb = eb.with(ProvidesHealing{ heal_amount: effect.1.parse::<i32>().unwrap() }) } "ranged" => { eb = eb.with(Ranged{ range: effect.1.parse::<i32>().unwrap() }) }, "damage" => { eb = eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }) } "area_of_effect" => { eb = eb.with(AreaOfEffect{ radius: effect.1.parse::<i32>().unwrap() }) } "confusion" => { eb = eb.with(Confusion{ turns: effect.1.parse::<i32>().unwrap() }) } "magic_mapping" => { eb = eb.with(MagicMapper{}) } "food" => { eb = eb.with(ProvidesFood{}) } _ => { rltk::console::log(format!("Warning: consumable effect {} not implemented.", effect_name)); } } } } }
您现在可以从 spawner.rs
中删除火球术、魔法地图和混乱卷轴了!运行游戏,您就可以访问这些物品了。希望这开始说明了将数据文件链接到组件创建的强大功能。
添加剩余的物品
我们将在 spawns.json
中再添加几个 JSON 条目,以涵盖我们剩余的各种其他物品:
{
"name" : "Dagger",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 2
}
},
{
"name" : "Longsword",
"renderable": {
"glyph" : "/",
"fg" : "#FFAAFF",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 4
}
},
{
"name" : "Shield",
"renderable": {
"glyph" : "[",
"fg" : "#00AAFF",
"bg" : "#000000",
"order" : 2
},
"shield" : {
"defense_bonus" : 1
}
},
{
"name" : "Tower Shield",
"renderable": {
"glyph" : "[",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"shield" : {
"defense_bonus" : 3
}
}
这里有两个新字段!shield
和 weapon
。我们需要扩展我们的 item_structs.rs
以处理它们:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Item { pub name : String, pub renderable : Option<Renderable>, pub consumable : Option<Consumable>, pub weapon : Option<Weapon>, pub shield : Option<Shield> } ... #[derive(Deserialize, Debug)] pub struct Weapon { pub range: String, pub power_bonus: i32 } #[derive(Deserialize, Debug)] pub struct Shield { pub defense_bonus: i32 } }
我们还需要教我们的 spawn_named_item
函数(在 rawmaster.rs
中)使用这些数据:
#![allow(unused)] fn main() { if let Some(weapon) = &item_template.weapon { eb = eb.with(Equippable{ slot: EquipmentSlot::Melee }); eb = eb.with(MeleePowerBonus{ power : weapon.power_bonus }); } if let Some(shield) = &item_template.shield { eb = eb.with(Equippable{ slot: EquipmentSlot::Shield }); eb = eb.with(DefenseBonus{ defense: shield.defense_bonus }); } }
您现在也可以从 spawner.rs
中删除这些物品,它们仍然会在游戏中生成 - 和以前一样。
现在是怪物了!
我们将在 spawns.json
中添加一个新的数组来处理怪物。我们称之为 “mobs” - 这是许多游戏中 “movable object”(可移动对象)的俚语,但它已经演变成在常用语中意味着四处移动并与您战斗的东西:
"mobs" : [
{
"name" : "Orc",
"renderable": {
"glyph" : "o",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 8
},
{
"name" : "Goblin",
"renderable": {
"glyph" : "g",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 8,
"hp" : 8,
"defense" : 1,
"power" : 3
},
"vision_range" : 8
}
]
您会注意到我们正在修复之前的一个小问题:兽人和哥布林的属性不再相同!否则,这应该是有意义的:我们在 spawner.rs
中设置的属性改为在 JSON 文件中设置。我们需要创建一个新文件 raws/mob_structs.rs
:
#![allow(unused)] fn main() { use serde::{Deserialize}; use super::{Renderable}; #[derive(Deserialize, Debug)] pub struct Mob { pub name : String, pub renderable : Option<Renderable>, pub blocks_tile : bool, pub stats : MobStats, pub vision_range : i32 } #[derive(Deserialize, Debug)] pub struct MobStats { pub max_hp : i32, pub hp : i32, pub power : i32, pub defense : i32 } }
我们还将修改 Raws
(目前在 item_structs.rs
中)。我们将把它移动到 mod.rs
,因为它与其他模块共享,并对其进行编辑:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Raws { pub items : Vec<Item>, pub mobs : Vec<Mob> } }
我们还需要修改 rawmaster.rs
以向构造函数添加一个空的 mobs
列表:
#![allow(unused)] fn main() { impl RawMaster { pub fn empty() -> RawMaster { RawMaster { raws : Raws{ items: Vec::new(), mobs: Vec::new() }, item_index : HashMap::new() } } ... }
我们还将修改 RawMaster
以索引我们的 mobs:
#![allow(unused)] fn main() { pub struct RawMaster { raws : Raws, item_index : HashMap<String, usize>, mob_index : HashMap<String, usize> } impl RawMaster { pub fn empty() -> RawMaster { RawMaster { raws : Raws{ items: Vec::new(), mobs: Vec::new() }, item_index : HashMap::new(), mob_index : HashMap::new() } } pub fn load(&mut self, raws : Raws) { self.raws = raws; self.item_index = HashMap::new(); for (i,item) in self.raws.items.iter().enumerate() { self.item_index.insert(item.name.clone(), i); } for (i,mob) in self.raws.mobs.iter().enumerate() { self.mob_index.insert(mob.name.clone(), i); } } } }
我们将要构建一个 spawn_named_mob
函数,但首先让我们创建一些助手函数,以便我们与 spawn_named_item
共享功能 - 避免重复自己。第一个非常直接:
#![allow(unused)] fn main() { fn spawn_position(pos : SpawnType, new_entity : EntityBuilder) -> EntityBuilder { let mut eb = new_entity; // 在指定位置生成 match pos { SpawnType::AtPosition{x,y} => { eb = eb.with(Position{ x, y }); } } eb } }
当我们添加更多 SpawnType
条目时,此函数必然会扩展以包含它们 - 因此它是一个函数 非常棒。我们可以用对这个函数的单个调用替换 spawn_named_item
中的相同代码:
#![allow(unused)] fn main() { // 在指定位置生成 eb = spawn_position(pos, eb); }
让我们也分离出 Renderable
数据的处理。这更困难;我在让 Rust 的生命周期检查器与实际将其添加到 EntityBuilder
的系统一起工作时遇到了 可怕的 时间。我最终确定了一个返回组件以供调用者添加的函数:
#![allow(unused)] fn main() { fn get_renderable_component(renderable : &super::item_structs::Renderable) -> crate::components::Renderable { crate::components::Renderable{ glyph: rltk::to_cp437(renderable.glyph.chars().next().unwrap()), fg : rltk::RGB::from_hex(&renderable.fg).expect("Invalid RGB"), bg : rltk::RGB::from_hex(&renderable.bg).expect("Invalid RGB"), render_order : renderable.order } } }
这仍然清理了 spawn_named_item
中的调用:
#![allow(unused)] fn main() { // Renderable if let Some(renderable) = &item_template.renderable { eb = eb.with(get_renderable_component(renderable)); } }
好的 - 有了这些,我们可以继续制作 spawn_named_mob
:
#![allow(unused)] fn main() { pub fn spawn_named_mob(raws: &RawMaster, new_entity : EntityBuilder, key : &str, pos : SpawnType) -> Option<Entity> { if raws.mob_index.contains_key(key) { let mob_template = &raws.raws.mobs[raws.mob_index[key]]; let mut eb = new_entity; // 在指定位置生成 eb = spawn_position(pos, eb); // Renderable if let Some(renderable) = &mob_template.renderable { eb = eb.with(get_renderable_component(renderable)); } eb = eb.with(Name{ name : mob_template.name.clone() }); eb = eb.with(Monster{}); if mob_template.blocks_tile { eb = eb.with(BlocksTile{}); } eb = eb.with(CombatStats{ max_hp : mob_template.stats.max_hp, hp : mob_template.stats.hp, power : mob_template.stats.power, defense : mob_template.stats.defense }); eb = eb.with(Viewshed{ visible_tiles : Vec::new(), range: mob_template.vision_range, dirty: true }); return Some(eb.build()); } None } }
这个函数中真的没有什么我们还没有介绍过的:我们只是应用一个 renderable、位置、名称,使用与之前相同的代码 - 然后检查 blocks_tile
以查看是否应该添加 BlocksTile
组件,并将属性复制到 CombatStats
组件中。我们还使用 vision_range
范围设置了一个 Viewshed
组件。
在我们再次更新 spawner.rs
之前,让我们引入一个主生成方法 - spawn_named_entity
。这背后的原因是生成系统实际上不知道(或不关心)实体是物品、mob 还是其他任何东西。与其在其中推送大量的 if
检查,不如提供一个单一的接口:
#![allow(unused)] fn main() { pub fn spawn_named_entity(raws: &RawMaster, new_entity : EntityBuilder, key : &str, pos : SpawnType) -> Option<Entity> { if raws.item_index.contains_key(key) { return spawn_named_item(raws, new_entity, key, pos); } else if raws.mob_index.contains_key(key) { return spawn_named_mob(raws, new_entity, key, pos); } None } }
所以在 spawner.rs
中,我们现在可以使用通用生成器:
#![allow(unused)] fn main() { let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y}); if spawn_result.is_some() { return; } }
我们也可以继续删除对兽人、哥布林和怪物的引用!我们快完成了 - 您现在可以获得数据驱动的怪物了。
门和陷阱
还有两个剩余的硬编码实体。这些一直被单独留下,因为它们与其他类型真的不一样:它们是我所说的 “props”(物件) - 关卡特征。您无法捡起它们,但它们是关卡不可或缺的一部分。因此,在 spawns.json
中,我们将继续定义一些 props:
"props" : [
{
"name" : "Bear Trap",
"renderable": {
"glyph" : "^",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 2
},
"hidden" : true,
"entry_trigger" : {
"effects" : {
"damage" : "6",
"single_activation" : "1"
}
}
},
{
"name" : "Door",
"renderable": {
"glyph" : "+",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false,
"blocks_tile" : true,
"blocks_visibility" : true,
"door_open" : true
}
]
props 的问题在于它们可能非常多样化,因此我们在定义中最终会得到很多 可选的 东西。我宁愿在 Rust 端而不是 JSON 端进行复杂的定义,以减少当我们有很多 props 时的大量输入。因此,我们最终在 JSON 中创建了一些相当富有表现力的东西,并做了大量工作使其在 Rust 中起作用!我们将创建一个新文件 prop_structs.rs
并将我们的序列化类放入其中:
#![allow(unused)] fn main() { use serde::{Deserialize}; use super::{Renderable}; use std::collections::HashMap; #[derive(Deserialize, Debug)] pub struct Prop { pub name : String, pub renderable : Option<Renderable>, pub hidden : Option<bool>, pub blocks_tile : Option<bool>, pub blocks_visibility : Option<bool>, pub door_open : Option<bool>, pub entry_trigger : Option<EntryTrigger> } #[derive(Deserialize, Debug)] pub struct EntryTrigger { pub effects : HashMap<String, String> } }
我们必须告诉 raws/mod.rs
使用它:
#![allow(unused)] fn main() { mod prop_structs; use prop_structs::*; }
我们还需要扩展 Raws
以保存它们:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Raws { pub items : Vec<Item>, pub mobs : Vec<Mob>, pub props : Vec<Prop> } }
这将我们带入 rawmaster.rs
,我们需要扩展构造函数和读取器以包含新类型:
#![allow(unused)] fn main() { pub struct RawMaster { raws : Raws, item_index : HashMap<String, usize>, mob_index : HashMap<String, usize>, prop_index : HashMap<String, usize> } impl RawMaster { pub fn empty() -> RawMaster { RawMaster { raws : Raws{ items: Vec::new(), mobs: Vec::new(), props: Vec::new() }, item_index : HashMap::new(), mob_index : HashMap::new(), prop_index : HashMap::new() } } pub fn load(&mut self, raws : Raws) { self.raws = raws; self.item_index = HashMap::new(); for (i,item) in self.raws.items.iter().enumerate() { self.item_index.insert(item.name.clone(), i); } for (i,mob) in self.raws.mobs.iter().enumerate() { self.mob_index.insert(mob.name.clone(), i); } for (i,prop) in self.raws.props.iter().enumerate() { self.prop_index.insert(prop.name.clone(), i); } } } }
我们还创建了一个新函数 spawn_named_prop
:
#![allow(unused)] fn main() { pub fn spawn_named_prop(raws: &RawMaster, new_entity : EntityBuilder, key : &str, pos : SpawnType) -> Option<Entity> { if raws.prop_index.contains_key(key) { let prop_template = &raws.raws.props[raws.prop_index[key]]; let mut eb = new_entity; // 在指定位置生成 eb = spawn_position(pos, eb); // Renderable if let Some(renderable) = &prop_template.renderable { eb = eb.with(get_renderable_component(renderable)); } eb = eb.with(Name{ name : prop_template.name.clone() }); if let Some(hidden) = prop_template.hidden { if hidden { eb = eb.with(Hidden{}) }; } if let Some(blocks_tile) = prop_template.blocks_tile { if blocks_tile { eb = eb.with(BlocksTile{}) }; } if let Some(blocks_visibility) = prop_template.blocks_visibility { if blocks_visibility { eb = eb.with(BlocksVisibility{}) }; } if let Some(door_open) = prop_template.door_open { eb = eb.with(Door{ open: door_open }); } if let Some(entry_trigger) = &prop_template.entry_trigger { eb = eb.with(EntryTrigger{}); for effect in entry_trigger.effects.iter() { match effect.0.as_str() { "damage" => { eb = eb.with(InflictsDamage{ damage : effect.1.parse::<i32>().unwrap() }) } "single_activation" => { eb = eb.with(SingleActivation{}) } _ => {} } } } return Some(eb.build()); } None } }
我们将略过内容,因为这基本上与我们之前所做的相同。我们需要扩展 spawn_named_entity
以包含 props:
#![allow(unused)] fn main() { pub fn spawn_named_entity(raws: &RawMaster, new_entity : EntityBuilder, key : &str, pos : SpawnType) -> Option<Entity> { if raws.item_index.contains_key(key) { return spawn_named_item(raws, new_entity, key, pos); } else if raws.mob_index.contains_key(key) { return spawn_named_mob(raws, new_entity, key, pos); } else if raws.prop_index.contains_key(key) { return spawn_named_prop(raws, new_entity, key, pos); } None } }
最后,我们可以进入 spawner.rs
并删除门和熊陷阱函数。我们可以完成清理 spawn_entity
函数。我们还将添加一个警告,以防您尝试生成一些我们不知道的东西:
#![allow(unused)] fn main() { /// 在 (tuple.0) 位置生成一个命名实体(tuple.1 中的名称) pub fn spawn_entity(ecs: &mut World, spawn : &(&usize, &String)) { let map = ecs.fetch::<Map>(); let width = map.width as usize; let x = (*spawn.0 % width) as i32; let y = (*spawn.0 / width) as i32; std::mem::drop(map); let spawn_result = spawn_named_entity(&RAWS.lock().unwrap(), ecs.create_entity(), &spawn.1, SpawnType::AtPosition{ x, y}); if spawn_result.is_some() { return; } rltk::console::log(format!("WARNING: We don't know how to spawn [{}]!", spawn.1)); } }
如果您现在 cargo run
,您将看到门和陷阱像以前一样工作。
总结
本章使我们能够轻松更改装饰我们关卡的物品、mobs 和 props。我们尚未涉及 添加更多(或调整生成表) - 那将是下一章的内容。您现在可以快速更改游戏的特性;想要哥布林变得更弱吗?降低他们的属性!想要它们比兽人有更好的视力吗?调整它们的视野范围!这就是数据驱动方法的主要好处:您可以快速进行更改,而无需深入研究源代码。引擎 负责 模拟世界 - 而 数据 负责 描述世界。
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。