填充起始城镇


关于本教程

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

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

Hands-On Rust


在上一章中,我们构建了城镇的布局。 在本章中,我们将用 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()));
            }
        }
    }
}
}

让我们逐步了解一下:

  1. 该函数接受我们的建筑物数据、地图信息和随机数生成器作为参数。
  2. 由于我们总是让玩家在酒馆中开始,所以我们在这里执行此操作。 我们可以从 build 函数中删除它。
  3. 我们存储 player_idx - 我们不想在玩家之上生成任何东西。
  4. 我们创建 to_place - 我们想要在酒吧中的字符串标签列表。 我们稍后会担心编写这些内容。
  5. 我们在整个建筑物中迭代 xy
    1. 我们计算建筑物瓦片的地图索引。
    2. 如果建筑物瓦片是木地板 (WoodFloor),地图索引不是玩家地图索引,并且 1d3 掷骰结果为 1,我们:
      1. to_place 列表中取出第一个标签,并从列表中删除它(除非我们放入两次,否则没有重复项)。
      2. 使用当前瓦片标签将该标签添加到地图的 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.rssaveload_system.rs 中注册它!

如果您现在 cargo run,您应该会看到一个微笑的酒保。 他身穿紫色(来自 JSON 的 RGB #EE82EE)显得容光焕发。 为什么要用紫色? 我们最终会将供应商变成紫色(供应商将在以后的章节中介绍):

Screenshot

他不会对你做出反应或任何事情,但他就在那里。 我们将在本章稍后添加一些行为。 现在,既然我们支持无辜的旁观者(专业提示:复制现有条目并编辑它;比全部重新输入容易得多),让我们继续在 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,酒吧会变得更热闹一些:

Screenshot

添加道具 (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,您会看到一些惰性道具散落在酒馆中:

Screenshot

这并不令人惊叹,但已经感觉更有生气了!

制作神庙 (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,您可以四处奔跑并找到一座神庙:

Screenshot

构建其他建筑物 (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,您可以四处奔跑并找到人口稠密的房间:

Screenshot

希望您也发现了错误:玩家击败了他/她的妈妈(和炼金术士)! 我们真的不想鼓励这种类型的行为! 因此,在下一节中,我们将研究一些中立的 AI 和玩家与 NPC 的移动行为。

中立 AI/移动 (Neutral AI/Movement)

我们当前的“旁观者 (bystander)”处理存在两个问题:旁观者只是像木头一样站在那里(甚至阻碍你的移动!),并且没有办法在不屠杀他们的情况下绕过他们。 我希望我们的英雄不会通过谋杀他们的母亲来开始他/她的冒险 - 所以让我们纠正这种情况!

交换位置 (Trading Places)

目前,当您“撞”到包含任何具有战斗属性的瓦片时 - 您会发起攻击。 这在 player.rstry_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; 撞到他们会交换你的位置:

Screenshot

废弃的房屋 (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,并四处寻找废弃的房屋 - 您会发现它充满了敌对的老鼠:

Screenshot

总结 (Wrap-Up)

在本章中,我们向城镇添加了一堆道具和旁观者 - 以及一间满是愤怒老鼠的房子。 这让它感觉更有生气了。 它绝不是已经完成,但它已经开始感觉像是一个幻想游戏的开场场景。 在下一章中,我们将进行一些 AI 调整,使其感觉更生动 - 并添加一些不方便地在建筑物内闲逛的旁观者。

本章的源代码可以在这里找到

使用 web assembly 在您的浏览器中运行本章的示例(需要 WebGL2)

版权所有 (C) 2019, Herbert Wolverson。