填充起始城镇
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您能喜欢本教程,并制作出伟大的游戏!
如果您喜欢本教程并希望我继续写作,请考虑支持我的 Patreon。
在上一章中,我们构建了城镇的布局。 在本章中,我们将用 NPC 和道具填充它。 我们将引入一些新的 AI 类型来处理友善或中立的 NPC,并开始放置商人、镇民和其他居民,使城镇充满生机。 我们还将开始放置家具和物品,以使这个地方感觉不那么荒凉。
识别建筑物
我们不是在制作一个真实的、全尺寸的城镇。 那里可能有数百座建筑物,玩家很快就会因为试图找到出口而感到厌烦。 相反 - 我们有 12 座建筑物。 查看我们的设计文档,其中两座很重要:
- 酒馆 (Pub)。
- 神庙 (Temple)。
剩下 10 个其他位置实际上并不重要,但我们暗示它们将包括供应商。 集思广益一些供应商,拥有以下内容是有意义的:
- 铁匠铺 (Blacksmith)(满足您的武器/盔甲需求)。
- 服装店 (Clothier)(用于服装、皮革和类似物品)。
- 炼金术士 (Alchemist)(用于药水、魔法物品和物品鉴定)。
所以我们还剩下 5 个位置要填充! 让我们把其中三个变成有居民的普通住宅 (homes),一个变成你的房子 (your house) - 配上一个唠叨的母亲,一个变成有啮齿动物问题的废弃房屋 (abandoned house)。 啮齿动物问题是幻想游戏的常见元素,当我们到达那一步时,它可能会成为一个很好的教程。
您会记得我们按大小对建筑物进行了排序,并确定最大的是酒馆。 让我们扩展一下,为每栋建筑物添加标签。 在 map_builders/town.rs
中,查看 build
函数,我们将扩展建筑物排序器。 首先,让我们为我们的建筑物类型创建一个 enum
:
#![allow(unused)] fn main() { enum BuildingTag { Pub, Temple, Blacksmith, Clothier, Alchemist, PlayerHouse, Hovel, Abandoned, Unassigned } }
接下来,我们将把我们的建筑物排序代码移到它自己的函数中(作为 TownBuilder
的一部分):
#![allow(unused)] fn main() { fn sort_buildings(&mut self, buildings: &[(i32, i32, i32, i32)]) -> Vec<(usize, i32, BuildingTag)> { let mut building_size : Vec<(usize, i32, BuildingTag)> = Vec::new(); for (i,building) in buildings.iter().enumerate() { building_size.push(( i, building.2 * building.3, BuildingTag::Unassigned )); } building_size.sort_by(|a,b| b.1.cmp(&a.1)); building_size[0].2 = BuildingTag::Pub; building_size[1].2 = BuildingTag::Temple; building_size[2].2 = BuildingTag::Blacksmith; building_size[3].2 = BuildingTag::Clothier; building_size[4].2 = BuildingTag::Alchemist; building_size[5].2 = BuildingTag::PlayerHouse; for b in building_size.iter_mut().skip(6) { b.2 = BuildingTag::Hovel; } let last_index = building_size.len()-1; building_size[last_index].2 = BuildingTag::Abandoned; building_size } }
这是我们之前的代码,添加了 BuildingTag
条目。 一旦我们按大小排序,我们就分配各种建筑物类型 - 最后一个始终是废弃的房屋。 这将确保我们拥有所有建筑物类型,并且它们按降序排列。
在 build
函数中,用对该函数的调用替换您的排序代码 - 以及对 building_factory
的调用,我们稍后会编写它:
#![allow(unused)] fn main() { let building_size = self.sort_buildings(&buildings); self.building_factory(rng, build_data, &buildings, &building_size); }
现在我们将构建一个骨架工厂:
#![allow(unused)] fn main() { fn building_factory(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap, buildings: &[(i32, i32, i32, i32)], building_index : &[(usize, i32, BuildingTag)]) { for (i,building) in buildings.iter().enumerate() { let build_type = &building_index[i].2; match build_type { _ => {} } } } }
酒馆 (The Pub)
那么,当你在早上醒来,宿醉且惊讶地发现自己已经答应拯救世界时,你期望在酒馆里找到什么? 脑海中浮现出一些想法:
- 其他宿醉的顾客 (patrons),可能睡着了。
- 一个尽可能可疑的“丢失”货物销售员 (salesperson)。
- 一个酒保 (Barkeep),他可能希望你回家。
- 桌子、椅子、桶 (barrels)。
我们将扩展我们的工厂函数,添加一个 match
行来构建酒馆:
#![allow(unused)] fn main() { fn building_factory(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap, buildings: &[(i32, i32, i32, i32)], building_index : &[(usize, i32, BuildingTag)]) { for (i,building) in buildings.iter().enumerate() { let build_type = &building_index[i].2; match build_type { BuildingTag::Pub => self.build_pub(&building, build_data, rng), _ => {} } } } }
我们将开始编写新函数 build_pub
:
#![allow(unused)] fn main() { fn build_pub(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置玩家 (Place the player) build_data.starting_position = Some(Position{ x : building.0 + (building.2 / 2), y : building.1 + (building.3 / 2) }); let player_idx = build_data.map.xy_idx(building.0 + (building.2 / 2), building.1 + (building.3 / 2)); // 放置其他物品 (Place other items) let mut to_place : Vec<&str> = vec!["Barkeep", "Shady Salesman", "Patron", "Patron", "Keg", "Table", "Chair", "Table", "Chair"]; for y in building.1 .. building.1 + building.3 { for x in building.0 .. building.0 + building.2 { let idx = build_data.map.xy_idx(x, y); if build_data.map.tiles[idx] == TileType::WoodFloor && idx != player_idx && rng.roll_dice(1, 3)==1 && !to_place.is_empty() { let entity_tag = to_place[0]; to_place.remove(0); build_data.spawn_list.push((idx, entity_tag.to_string())); } } } } }
让我们逐步了解一下:
- 该函数接受我们的建筑物数据、地图信息和随机数生成器作为参数。
- 由于我们总是让玩家在酒馆中开始,所以我们在这里执行此操作。 我们可以从
build
函数中删除它。 - 我们存储
player_idx
- 我们不想在玩家之上生成任何东西。 - 我们创建
to_place
- 我们想要在酒吧中的字符串标签列表。 我们稍后会担心编写这些内容。 - 我们在整个建筑物中迭代
x
和y
。- 我们计算建筑物瓦片的地图索引。
- 如果建筑物瓦片是木地板 (WoodFloor),地图索引不是玩家地图索引,并且 1d3 掷骰结果为 1,我们:
- 从
to_place
列表中取出第一个标签,并从列表中删除它(除非我们放入两次,否则没有重复项)。 - 使用当前瓦片标签将该标签添加到地图的
spawn_list
中。
- 从
这非常简单,并且部分内容绝对足够通用,可以帮助处理未来的建筑物。 如果您现在运行项目,您将看到如下错误消息:WARNING: We don't know how to spawn [Barkeep]!
。 这是因为我们还没有编写它们。 我们需要 spawns.json
来包含我们尝试生成的所有标签。
制作非敌对 NPC (Making non-hostile NPCs)
让我们为我们的酒保 (Barkeep) 在 spawns.json
中添加一个条目。 我们将引入一个新元素 - ai
:
"mobs" : [
{
"name" : "Barkeep",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
为了支持 AI 元素,我们需要打开 raws/mob_structs.rs
并编辑 Mob
:
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Mob { pub name : String, pub renderable : Option<Renderable>, pub blocks_tile : bool, pub stats : MobStats, pub vision_range : i32, pub ai : String } }
我们还需要将 "ai" : "melee"
添加到每个其他 mob 中。 现在打开 raws/rawmaster.rs
,我们将编辑 spawn_named_mob
以支持它。 将行 eb = eb.with(Monster{});
替换为:
#![allow(unused)] fn main() { match mob_template.ai.as_ref() { "melee" => eb = eb.with(Monster{}), "bystander" => eb = eb.with(Bystander{}), _ => {} } }
Bystander
是一个新的组件 - 所以我们需要打开 components.rs
并添加它:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Bystander {} }
然后不要忘记在 main.rs
和 saveload_system.rs
中注册它!
如果您现在 cargo run
,您应该会看到一个微笑的酒保。 他身穿紫色(来自 JSON 的 RGB #EE82EE
)显得容光焕发。 为什么要用紫色? 我们最终会将供应商变成紫色(供应商将在以后的章节中介绍):
他不会对你做出反应或做任何事情,但他就在那里。 我们将在本章稍后添加一些行为。 现在,既然我们支持无辜的旁观者(专业提示:复制现有条目并编辑它;比全部重新输入容易得多),让我们继续在 spawns.json
中添加一些其他实体:
{
"name" : "Shady Salesman",
"renderable": {
"glyph" : "h",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Patron",
"renderable": {
"glyph" : "☺",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
如果您现在 cargo run
,酒吧会变得更热闹一些:
添加道具 (Adding props)
一个有人的酒馆,但没有任何东西让他们喝酒、坐着或吃东西,这是一个非常简陋的酒馆。 我想我们可以争辩说这是一个真正的烂地方,预算不够,但是当你开始添加其他建筑物时,这种说法就变得苍白无力了。 所以我们将在 spawns.json
中添加一些道具:
{
"name" : "Keg",
"renderable": {
"glyph" : "φ",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Table",
"renderable": {
"glyph" : "╦",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Chair",
"renderable": {
"glyph" : "└",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}
如果您现在 cargo run
,您会看到一些惰性道具散落在酒馆中:
这并不令人惊叹,但已经感觉更有生气了!
制作神庙 (Making the temple)
就生成代码而言,神庙将与酒馆类似。 事实上,非常相似,以至于我们将从 build_pub
函数中分离出生成实体的部分,并从中创建一个通用函数。 这是新函数:
#![allow(unused)] fn main() { fn random_building_spawn( &mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator, to_place : &mut Vec<&str>, player_idx : usize) { for y in building.1 .. building.1 + building.3 { for x in building.0 .. building.0 + building.2 { let idx = build_data.map.xy_idx(x, y); if build_data.map.tiles[idx] == TileType::WoodFloor && idx != player_idx && rng.roll_dice(1, 3)==1 && !to_place.is_empty() { let entity_tag = to_place[0]; to_place.remove(0); build_data.spawn_list.push((idx, entity_tag.to_string())); } } } } }
我们将 build_pub
中对该代码的调用替换为:
#![allow(unused)] fn main() { // 放置其他物品 (Place other items) let mut to_place : Vec<&str> = vec!["Barkeep", "Shady Salesman", "Patron", "Patron", "Keg", "Table", "Chair", "Table", "Chair"]; self.random_building_spawn(building, build_data, rng, &mut to_place, player_idx); }
有了这个,让我们思考一下你在神庙里可能会发现什么:
- 牧师 (Priests)
- 教区居民 (Parishioners)
- 椅子 (Chairs)
- 蜡烛 (Candles)
现在我们将扩展我们的工厂以包括神庙:
#![allow(unused)] fn main() { match build_type { BuildingTag::Pub => self.build_pub(&building, build_data, rng), BuildingTag::Temple => self.build_temple(&building, build_data, rng), _ => {} } }
我们的 build_temple
函数可以非常简单:
#![allow(unused)] fn main() { fn build_temple(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Priest", "Parishioner", "Parishioner", "Chair", "Chair", "Candle", "Candle"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } }
因此,有了这些 - 我们仍然必须将牧师、教区居民和蜡烛添加到 spawns.json
列表中。 牧师和教区居民在 mobs
部分中,并且基本上与酒保相同:
{
"name" : "Priest",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Parishioner",
"renderable": {
"glyph" : "☺",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
同样,至少现在 - 蜡烛只是另一种道具:
{
"name" : "Candle",
"renderable": {
"glyph" : "Ä",
"fg" : "#FFA500",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}
如果您现在 cargo run
,您可以四处奔跑并找到一座神庙:
构建其他建筑物 (Build other buildings)
我们现在已经完成了大部分艰苦的工作,所以我们只是在填补空白。 让我们扩展构建器中的 match
,以包含除废弃房屋之外的各种类型:
#![allow(unused)] fn main() { let build_type = &building_index[i].2; match build_type { BuildingTag::Pub => self.build_pub(&building, build_data, rng), BuildingTag::Temple => self.build_temple(&building, build_data, rng), BuildingTag::Blacksmith => self.build_smith(&building, build_data, rng), BuildingTag::Clothier => self.build_clothier(&building, build_data, rng), BuildingTag::Alchemist => self.build_alchemist(&building, build_data, rng), BuildingTag::PlayerHouse => self.build_my_house(&building, build_data, rng), BuildingTag::Hovel => self.build_hovel(&building, build_data, rng), _ => {} } }
我们将这些组合在一起,因为它们基本上是相同的函数! 这是它们每个函数的主体:
#![allow(unused)] fn main() { fn build_smith(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Blacksmith", "Anvil", "Water Trough", "Weapon Rack", "Armor Stand"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } fn build_clothier(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Clothier", "Cabinet", "Table", "Loom", "Hide Rack"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } fn build_alchemist(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Alchemist", "Chemistry Set", "Dead Thing", "Chair", "Table"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } fn build_my_house(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Mom", "Bed", "Cabinet", "Chair", "Table"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } fn build_hovel(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { // 放置物品 (Place items) let mut to_place : Vec<&str> = vec!["Peasant", "Bed", "Chair", "Table"]; self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } }
正如您所看到的 - 这些基本上是将生成列表传递给建筑物生成器,而不是做任何太花哨的事情。 我们在这里创建了很多新实体! 我试图想出你可能在每个位置找到的东西:
- 铁匠铺 (smith) 当然有一个铁匠 (Blacksmith)。 他喜欢围绕着铁砧 (Anvils)、水槽 (Water Troughs)、武器架 (Weapon Racks) 和盔甲架 (Armor Stands)。
- 服装店 (clothier) 有一个服装商 (Clothier),一个柜子 (Cabinet)、一张桌子 (Table)、一台织布机 (Loom) 和一个兽皮架 (Hide Rack)。
- 炼金术士 (alchemist) 有一个炼金术士 (Alchemist),一套化学装置 (Chemistry Set),一个死物 (Dead Thing)(为什么不呢?),一把椅子 (Chair) 和一张桌子 (Table)。
- 我的房子 (My House) 以妈妈 (Mom)(角色的母亲!)为特色,一张床 (bed)、一个柜子 (cabinet)、一把椅子 (chair) 和一张桌子 (table)。
- 小屋 (Hovels) 以农民 (Peasant)、一张床 (bed)、一把椅子 (chair) 和一张桌子 (table) 为特色。
因此,我们需要在 spawns.json
中支持这些:
{
"name" : "Blacksmith",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Clothier",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Alchemist",
"renderable": {
"glyph" : "☺",
"fg" : "#EE82EE",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Mom",
"renderable": {
"glyph" : "☺",
"fg" : "#FFAAAA",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
{
"name" : "Peasant",
"renderable": {
"glyph" : "☺",
"fg" : "#999999",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 16,
"hp" : 16,
"defense" : 1,
"power" : 4
},
"vision_range" : 4,
"ai" : "bystander"
},
在道具部分:
{
"name" : "Anvil",
"renderable": {
"glyph" : "╔",
"fg" : "#AAAAAA",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Water Trough",
"renderable": {
"glyph" : "•",
"fg" : "#5555FF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Weapon Rack",
"renderable": {
"glyph" : "π",
"fg" : "#FFD700",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Armor Stand",
"renderable": {
"glyph" : "⌠",
"fg" : "#FFFFFF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Chemistry Set",
"renderable": {
"glyph" : "δ",
"fg" : "#00FFFF",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Dead Thing",
"renderable": {
"glyph" : "☻",
"fg" : "#AA0000",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Cabinet",
"renderable": {
"glyph" : "∩",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Bed",
"renderable": {
"glyph" : "8",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Loom",
"renderable": {
"glyph" : "≡",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
},
{
"name" : "Hide Rack",
"renderable": {
"glyph" : "π",
"fg" : "#805A46",
"bg" : "#000000",
"order" : 2
},
"hidden" : false
}
如果您现在 cargo run
,您可以四处奔跑并找到人口稠密的房间:
希望您也发现了错误:玩家击败了他/她的妈妈(和炼金术士)! 我们真的不想鼓励这种类型的行为! 因此,在下一节中,我们将研究一些中立的 AI 和玩家与 NPC 的移动行为。
中立 AI/移动 (Neutral AI/Movement)
我们当前的“旁观者 (bystander)”处理存在两个问题:旁观者只是像木头一样站在那里(甚至阻碍你的移动!),并且没有办法在不屠杀他们的情况下绕过他们。 我希望我们的英雄不会通过谋杀他们的母亲来开始他/她的冒险 - 所以让我们纠正这种情况!
交换位置 (Trading Places)
目前,当您“撞”到包含任何具有战斗属性的瓦片时 - 您会发起攻击。 这在 player.rs
的 try_move_player
函数中提供:
#![allow(unused)] fn main() { let target = combat_stats.get(*potential_target); if let Some(_target) = target { wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed"); return; } }
我们需要扩展此功能,不仅要攻击,还要在撞到 NPC 时与他们交换位置。 这样,他们不会阻止你的移动 - 但你也不能谋杀你的母亲! 因此,首先,我们需要访问 Bystanders
组件存储,并创建一个向量,我们将在其中存储我们移动 NPC 的意图(我们不能只是在循环中访问它们;不幸的是,借用检查器会报错):
#![allow(unused)] fn main() { let bystanders = ecs.read_storage::<Bystander>(); let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new(); }
所以在 swap_entities
中,我们存储了要移动的实体及其 x/y 目标坐标。 现在我们调整主循环以检查目标是否是旁观者,将他们添加到交换列表,并在他们是旁观者的情况下仍然移动。 我们还使攻击取决于他们不是旁观者:
#![allow(unused)] fn main() { let bystander = bystanders.get(*potential_target); if bystander.is_some() { // 注意,我们想要移动旁观者 (Note that we want to move the bystander) swap_entities.push((*potential_target, pos.x, pos.y)); // 移动玩家 (Move the player) pos.x = min(map.width-1 , max(0, pos.x + delta_x)); pos.y = min(map.height-1, max(0, pos.y + delta_y)); entity_moved.insert(entity, EntityMoved{}).expect("Unable to insert marker"); viewshed.dirty = true; let mut ppos = ecs.write_resource::<Point>(); ppos.x = pos.x; ppos.y = pos.y; } else { let target = combat_stats.get(*potential_target); if let Some(_target) = target { wants_to_melee.insert(entity, WantsToMelee{ target: *potential_target }).expect("Add target failed"); return; } } }
最后,在该函数的最后,我们迭代 swap_entities
并应用移动:
#![allow(unused)] fn main() { for m in swap_entities.iter() { let their_pos = positions.get_mut(m.0); if let Some(their_pos) = their_pos { their_pos.x = m.1; their_pos.y = m.2; } } }
如果您现在 cargo run
,您将无法再谋杀所有 NPC; 撞到他们会交换你的位置:
废弃的房屋 (The Abandoned House)
最后(对于本章而言),我们需要填充废弃的房屋。 我们决定它将包含大量的啮齿动物问题,因为对于低级冒险者来说,异常大的啮齿动物是一个重要的问题! 我们将在我们的建筑物工厂匹配器中添加另一个匹配行:
#![allow(unused)] fn main() { BuildingTag::Abandoned => self.build_abandoned_house(&building, build_data, rng), }
这是用啮齿动物填充房屋大约一半的函数:
#![allow(unused)] fn main() { fn build_abandoned_house(&mut self, building: &(i32, i32, i32, i32), build_data : &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { for y in building.1 .. building.1 + building.3 { for x in building.0 .. building.0 + building.2 { let idx = build_data.map.xy_idx(x, y); if build_data.map.tiles[idx] == TileType::WoodFloor && idx != 0 && rng.roll_dice(1, 2)==1 { build_data.spawn_list.push((idx, "Rat".to_string())); } } } } }
最后,我们需要将 Rat
添加到 spawns.json
中的 mob 列表中:
{
"name" : "Rat",
"renderable": {
"glyph" : "r",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"stats" : {
"max_hp" : 2,
"hp" : 2,
"defense" : 1,
"power" : 3
},
"vision_range" : 8,
"ai" : "melee"
},
如果您现在 cargo run
,并四处寻找废弃的房屋 - 您会发现它充满了敌对的老鼠:
总结 (Wrap-Up)
在本章中,我们向城镇添加了一堆道具和旁观者 - 以及一间满是愤怒老鼠的房子。 这让它感觉更有生气了。 它绝不是已经完成,但它已经开始感觉像是一个幻想游戏的开场场景。 在下一章中,我们将进行一些 AI 调整,使其感觉更生动 - 并添加一些不方便地在建筑物内闲逛的旁观者。
本章的源代码可以在这里找到
使用 web assembly 在您的浏览器中运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。