数据驱动的生成表
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您能喜欢本教程,并制作出伟大的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持 我的 Patreon。
在上一章节中,我们将生成机制改为数据驱动:您在 JSON 数据文件中定义怪物、物品和道具 - 生成函数变成了一个解析器,它根据您的定义构建组件。这使您在数据驱动世界的道路上前进了一半。
如果您查看不断缩小的 spawner.rs
文件,我们会发现一个硬编码的表用于处理生成:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1 + map_depth) .add("Health Potion", 7) .add("Fireball Scroll", 2 + map_depth) .add("Confusion Scroll", 2 + map_depth) .add("Magic Missile Scroll", 4) .add("Dagger", 3) .add("Shield", 3) .add("Longsword", map_depth - 1) .add("Tower Shield", map_depth - 1) .add("Rations", 10) .add("Magic Mapping Scroll", 2) .add("Bear Trap", 5) } }
它在之前的章节中为我们提供了很好的服务,但遗憾的是,现在是时候让它退休了。我们希望能够在 JSON 数据中指定生成表 - 这样,我们可以向数据文件和生成列表添加新的实体,并且它们会出现在游戏中,而无需额外的 Rust 编码(除非它们需要新功能,在这种情况下,就需要扩展引擎)。
基于 JSON 的生成表
这是一个我设想的生成表示例:
"spawn_table" : [
{ "name" : "Goblin", "weight" : 10, "min_depth" : 0, "max_depth" : 100 }
],
所以 spawn_table
是一个数组,每个条目包含可以生成的东西。我们存储了可生成物的 name (名称)。我们给它一个 weight (权重),这与我们当前 RandomTable
结构中的相同字段相对应。我们添加了 min_depth
(最小深度) 和 max_depth
(最大深度) - 因此此生成行将仅应用于地下城的指定深度范围。
这看起来不错,所以让我们把我们所有的实体都放进去:
"spawn_table" : [
{ "name" : "Goblin", "weight" : 10, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Orc", "weight" : 1, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
{ "name" : "Health Potion", "weight" : 7, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Fireball Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
{ "name" : "Confusion Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100, "add_map_depth_to_weight" : true },
{ "name" : "Magic Missile Scroll", "weight" : 4, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Dagger", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Shield", "weight" : 3, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Longsword", "weight" : 1, "min_depth" : 1, "max_depth" : 100 },
{ "name" : "Tower Shield", "weight" : 1, "min_depth" : 1, "max_depth" : 100 },
{ "name" : "Rations", "weight" : 10, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Magic Mapping Scroll", "weight" : 2, "min_depth" : 0, "max_depth" : 100 },
{ "name" : "Bear Trap", "weight" : 5, "min_depth" : 0, "max_depth" : 100 }
],
请注意,我们添加了 add_map_depth_to_weight
,以便我们可以指示事物在游戏后期变得更有可能出现。这使我们能够保持可变权重的能力。我们还将 longsword (长剑) 和 tower shield (塔盾) 设置为仅在第一层之后出现。
这非常全面(涵盖了我们目前拥有的所有内容,并增加了一些功能),所以让我们在 raws
中创建一个新文件 spawn_table_structs
,并定义读取这些数据所需的类:
#![allow(unused)] fn main() { use serde::{Deserialize}; use super::{Renderable}; #[derive(Deserialize, Debug)] pub struct SpawnTableEntry { pub name : String, pub weight : i32, pub min_depth: i32, pub max_depth: i32, pub add_map_depth_to_weight : Option<bool> } }
打开 raws/mod.rs
,我们会将其添加到 Raws
结构中:
#![allow(unused)] fn main() { mod spawn_table_structs; use spawn_table_structs::*; ... #[derive(Deserialize, Debug)] pub struct Raws { pub items : Vec<Item>, pub mobs : Vec<Mob>, pub props : Vec<Prop>, pub spawn_table : Vec<SpawnTableEntry> } }
我们还需要将其添加到 rawmaster.rs
中的构造函数中:
#![allow(unused)] fn main() { pub fn empty() -> RawMaster { RawMaster { raws : Raws{ items: Vec::new(), mobs: Vec::new(), props: Vec::new(), spawn_table: Vec::new() }, item_index : HashMap::new(), mob_index : HashMap::new(), prop_index : HashMap::new(), } } }
现在值得快速运行 cargo run
,以确保生成表加载时没有错误。它现在还不会 做 任何事情,但总是很高兴知道数据加载正确。
使用新的生成表
在 rawmaster.rs
中,我们将添加一个新函数,用于从我们的 JSON 数据构建随机生成表:
#![allow(unused)] fn main() { pub fn get_spawn_table_for_depth(raws: &RawMaster, depth: i32) -> RandomTable { use super::SpawnTableEntry; let available_options : Vec<&SpawnTableEntry> = raws.raws.spawn_table .iter() .filter(|a| depth >= a.min_depth && depth <= a.max_depth) .collect(); let mut rt = RandomTable::new(); for e in available_options.iter() { let mut weight = e.weight; if e.add_map_depth_to_weight.is_some() { weight += depth; } rt = rt.add(e.name.clone(), weight); } rt } }
这个函数非常简单:
- 我们获取
raws.raws.spawn_table
- 这是主生成表列表。 - 我们使用
iter()
获取一个迭代器。 - 我们使用
filter
仅包含在请求的地图深度范围内的项目。 - 我们
collect()
将其收集到一个SpawnTableEntry
行的引用向量中。 - 我们迭代所有收集到的可用选项:
- 我们获取权重。
- 如果条目具有“增加地图深度到权重”的要求,我们将该深度添加到该条目的权重。
- 我们将其添加到我们的
RandomTable
中。
非常直接!我们可以打开 spawner.rs
并修改我们的 RoomTable
函数以使用它:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { get_spawn_table_for_depth(&RAWS.lock().unwrap(), map_depth) } }
哇,这是一个简短的函数!但它完成了工作。如果您现在 cargo run
,您将像以前一样玩游戏。
添加一些健全性检查
我们现在已经具备了添加实体而无需接触我们的 Rust 代码的能力!在我们探索这一点之前,让我们看看向系统添加一些“健全性检查”以帮助避免错误。我们只需更改 rawmaster.rs
中的 load
函数:
#![allow(unused)] fn main() { pub fn load(&mut self, raws : Raws) { self.raws = raws; self.item_index = HashMap::new(); let mut used_names : HashSet<String> = HashSet::new(); for (i,item) in self.raws.items.iter().enumerate() { if used_names.contains(&item.name) { rltk::console::log(format!("WARNING - duplicate item name in raws [{}]", item.name)); } self.item_index.insert(item.name.clone(), i); used_names.insert(item.name.clone()); } for (i,mob) in self.raws.mobs.iter().enumerate() { if used_names.contains(&mob.name) { rltk::console::log(format!("WARNING - duplicate mob name in raws [{}]", mob.name)); } self.mob_index.insert(mob.name.clone(), i); used_names.insert(mob.name.clone()); } for (i,prop) in self.raws.props.iter().enumerate() { if used_names.contains(&prop.name) { rltk::console::log(format!("WARNING - duplicate prop name in raws [{}]", prop.name)); } self.prop_index.insert(prop.name.clone(), i); used_names.insert(prop.name.clone()); } for spawn in self.raws.spawn_table.iter() { if !used_names.contains(&spawn.name) { rltk::console::log(format!("WARNING - Spawn tables references unspecified entity {}", spawn.name)); } } } }
我们在这里做什么?我们创建一个 used_names
作为 HashSet
。每当我们加载某些东西时,我们都会将其添加到集合中。如果它已经存在?那么我们已经制作了重复项,并且会发生不好的事情 - 所以我们警告用户。然后我们迭代生成表,如果我们引用了尚未定义的实体名称 - 我们再次警告用户。
这些类型的数据输入错误很常见,并且实际上不会使程序崩溃。这种健全性检查确保我们至少在继续认为一切都很好之前收到警告。如果您有偏执狂(在编程时,这实际上是一个很好的特质;有很多人想抓住您!),您可以将 println!
替换为 panic!
并崩溃,而不是仅仅提醒用户。如果您喜欢经常 cargo run
以查看您的进展,您可能不想这样做!
从数据驱动架构中受益
让我们快速向游戏中添加一种新武器和一个新怪物。我们可以做到这一点,而无需接触 Rust
代码,只需重新编译(嵌入更改的文件)即可。在 spawns.json
中,让我们在武器列表中添加一个 Battleaxe
(战斧):
{
"name" : "Battleaxe",
"renderable": {
"glyph" : "¶",
"fg" : "#FF55FF",
"bg" : "#000000",
"order" : 2
},
"weapon" : {
"range" : "melee",
"power_bonus" : 5
}
},
我们也会将其添加到生成表中:
{ "name" : "Battleaxe", "weight" : 1, "min_depth" : 2, "max_depth" : 100 }
让我们也添加一个卑微的 kobold (狗头人)。它基本上是一个更弱的 goblin (哥布林)。我们喜欢狗头人,让我们多要点!
{
"name" : "Kobold",
"renderable": {
"glyph" : "k",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 4,
"hp" : 4,
"defense" : 0,
"power" : 2
},
"vision_range" : 4
}
所以我们也会将这个小生物添加到生成列表中:
{ "name" : "Kobold", "weight" : 15, "min_depth" : 0, "max_depth" : 3 }
请注意,我们让他们 非常 常见 - 并在 3 级之后停止用它们骚扰玩家。
如果您现在 cargo run
该项目,您将在游戏中找到新实体:
.
总结
生成表就到此为止了!在过去的两个章节中,您获得了相当大的力量 - 请明智地使用它。您现在可以添加各种实体,而无需编写一行 Rust 代码,并且可以轻松地开始将游戏塑造成您想要的样子。在下一章中,我们将开始这样做。
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例 (需要 WebGL2)
版权 (C) 2019, Herbert Wolverson.