恶龙现身
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
现在我们有了法术和更高级的物品能力,玩家或许能够在我们第 4.17 节中构建的矮人要塞的第一层中生存下来了!此外,既然 NPC 也可以使用特殊能力了 - 我们应该能够模拟(根据我们的设计文档)占据矮人要塞的邪恶巨龙了!本章将完全关于构建龙穴,填充它,并使龙成为一个可怕但可击败的敌人。
构建龙穴
根据设计文档,第六层曾经是一个强大的矮人要塞 - 但已被一条邪恶的黑龙占据,这条龙非常喜欢吃冒险者(并且大概也解决了过去的矮人)。这意味着我们想要的是一个基于走廊的地牢 - 矮人往往喜欢这种风格,但被侵蚀和爆破成了龙穴的样子。
为了辅助实现这一点,我们将重新启用地图生成观察。在 main.rs
中,将切换开关更改为 true:
#![allow(unused)] fn main() { const SHOW_MAPGEN_VISUALIZER : bool = true; // 启用地图生成可视化器 }
接下来,我们将组合一个骨架来构建关卡。在 map_builders/mod.rs
中,添加以下内容:
#![allow(unused)] fn main() { mod dwarf_fort_builder; use dwarf_fort_builder::*; }
并更新末尾的函数:
#![allow(unused)] fn main() { pub fn level_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { rltk::console::log(format!("Depth: {}", new_depth)); match new_depth { 1 => town_builder(new_depth, rng, width, height), 2 => forest_builder(new_depth, rng, width, height), 3 => limestone_cavern_builder(new_depth, rng, width, height), 4 => limestone_deep_cavern_builder(new_depth, rng, width, height), 5 => limestone_transition_builder(new_depth, rng, width, height), 6 => dwarf_fort_builder(new_depth, rng, width, height), _ => random_builder(new_depth, rng, width, height) } } }
现在,我们将创建一个新文件 - map_builders/dwarf_fort.rs
,并在其中放入一个基于最小 BSP 的房间地牢:
#![allow(unused)] fn main() { use super::{BuilderChain, XStart, YStart, AreaStartingPosition, RoomSorter, RoomSort, CullUnreachable, VoronoiSpawning, BspDungeonBuilder, DistantExit, BspCorridors, CorridorSpawner, RoomDrawer}; pub fn dwarf_fort_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { let mut chain = BuilderChain::new(new_depth, width, height, "Dwarven Fortress"); chain.start_with(BspDungeonBuilder::new()); chain.with(RoomSorter::new(RoomSort::CENTRAL)); chain.with(RoomDrawer::new()); chain.with(BspCorridors::new()); chain.with(CorridorSpawner::new()); chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP)); chain.with(CullUnreachable::new()); chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::BOTTOM)); chain.with(VoronoiSpawning::new()); chain.with(DistantExit::new()); chain } }
这将为您提供一个非常基本的基于房间的地牢:
这是一个好的开始,但并不完全是我们想要的。它显然是人(矮人!)造的,但它没有“可怕的龙住在这里”的氛围。因此,我们也将制作一个看起来可怕的地图,其中包含更大的中心区域,并将两者合并在一起。我们想要一种有点不祥的感觉,所以我们将制作一个自定义构建器层来生成 DLA Insectoid 地图并将其合并进来:
#![allow(unused)] fn main() { pub struct DragonsLair {} impl MetaMapBuilder for DragonsLair { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl DragonsLair { #[allow(dead_code)] pub fn new() -> Box<DragonsLair> { Box::new(DragonsLair{}) } fn build(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { build_data.map.depth = 7; build_data.take_snapshot(); let mut builder = BuilderChain::new(6, build_data.width, build_data.height, "New Map"); builder.start_with(DLABuilder::insectoid()); builder.build_map(rng); // 将历史记录添加到我们的历史记录中 for h in builder.build_data.history.iter() { build_data.history.push(h.clone()); } build_data.take_snapshot(); // 合并地图 for (idx, tt) in build_data.map.tiles.iter_mut().enumerate() { if *tt == TileType::Wall && builder.build_data.map.tiles[idx] == TileType::Floor { *tt = TileType::Floor; } } build_data.take_snapshot(); } } }
我们将它添加到我们的构建器函数中:
#![allow(unused)] fn main() { pub fn dwarf_fort_builder(new_depth: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { let mut chain = BuilderChain::new(new_depth, width, height, "Dwarven Fortress"); chain.start_with(BspDungeonBuilder::new()); chain.with(RoomSorter::new(RoomSort::CENTRAL)); chain.with(RoomDrawer::new()); chain.with(BspCorridors::new()); chain.with(CorridorSpawner::new()); chain.with(DragonsLair::new()); chain.with(AreaStartingPosition::new(XStart::LEFT, YStart::TOP)); chain.with(CullUnreachable::new()); chain.with(AreaEndingPosition::new(XEnd::RIGHT, YEnd::BOTTOM)); chain.with(VoronoiSpawning::new()); chain.with(DistantExit::new()); chain } }
这给出了一个更合适的地图:
您会注意到我们从一个对角角落开始,到另一个对角角落结束 - 目的是让玩家难以完全避开中间区域!
常规生成
如果您现在 cargo run
,您会注意到关卡主要充满了物品 - 免费的战利品! - 而怪物却不多。实际上这里发生了两件事:我们将物品和怪物的权重放在同一个表中,并且我们最近添加了大量物品(相对于怪物和道具而言) - 而且这个关卡一开始就不允许有那么多怪物。随着我们添加越来越多的物品,这个问题只会变得更糟!所以让我们修复它。
让我们首先打开 raws/rawmaster.rs
并添加一个新函数来确定条目的生成类型:
#![allow(unused)] fn main() { pub enum SpawnTableType { Item, Mob, Prop } pub fn spawn_type_by_name(raws: &RawMaster, key : &str) -> SpawnTableType { if raws.item_index.contains_key(key) { SpawnTableType::Item } else if raws.mob_index.contains_key(key) { SpawnTableType::Mob } else { SpawnTableType::Prop } } }
我们将向 random_table.rs
文件添加一个新的 MasterTable
结构。它充当表的持有者,按物品类型排序。我们还将修复先前版本中一些奇怪的布局:
#![allow(unused)] fn main() { use rltk::RandomNumberGenerator; use crate::raws::{SpawnTableType, spawn_type_by_name, RawMaster}; pub struct RandomEntry { name : String, weight : i32 } impl RandomEntry { pub fn new<S:ToString>(name: S, weight: i32) -> RandomEntry { RandomEntry{ name: name.to_string(), weight } } } #[derive(Default)] pub struct MasterTable { items : RandomTable, mobs : RandomTable, props : RandomTable } impl MasterTable { pub fn new() -> MasterTable { MasterTable{ items : RandomTable::new(), mobs : RandomTable::new(), props : RandomTable::new() } } pub fn add<S:ToString>(&mut self, name : S, weight: i32, raws: &RawMaster) { match spawn_type_by_name(raws, &name.to_string()) { SpawnTableType::Item => self.items.add(name, weight), SpawnTableType::Mob => self.mobs.add(name, weight), SpawnTableType::Prop => self.props.add(name, weight), } } pub fn roll(&self, rng : &mut RandomNumberGenerator) -> String { let roll = rng.roll_dice(1, 4); match roll { 1 => self.items.roll(rng), 2 => self.props.roll(rng), 3 => self.mobs.roll(rng), _ => "None".to_string() } } } #[derive(Default)] pub struct RandomTable { entries : Vec<RandomEntry>, total_weight : i32 } impl RandomTable { pub fn new() -> RandomTable { RandomTable{ entries: Vec::new(), total_weight: 0 } } pub fn add<S:ToString>(&mut self, name : S, weight: i32) { if weight > 0 { self.total_weight += weight; self.entries.push(RandomEntry::new(name.to_string(), weight)); } } pub fn roll(&self, rng : &mut RandomNumberGenerator) -> String { if self.total_weight == 0 { return "None".to_string(); } let mut roll = rng.roll_dice(1, self.total_weight)-1; let mut index : usize = 0; while roll > 0 { if roll < self.entries[index].weight { return self.entries[index].name.clone(); } roll -= self.entries[index].weight; index += 1; } "None".to_string() } } }
正如您所看到的,这会将可用的生成物按类型划分 - 然后滚动以选择要使用的表,然后再在表本身上滚动。现在在 rawmaster.rs
中,我们将修改 get_spawn_table_for_depth
函数以使用 master table:
#![allow(unused)] fn main() { pub fn get_spawn_table_for_depth(raws: &RawMaster, depth: i32) -> MasterTable { 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 = MasterTable::new(); for e in available_options.iter() { let mut weight = e.weight; if e.add_map_depth_to_weight.is_some() { weight += depth; } rt.add(e.name.clone(), weight, raws); } rt } }
由于我们已经为 MasterTable
实现了基本相同的接口,所以我们基本上可以保留现有代码 - 而只是使用新类型来代替。在 spawner.rs
中,我们还需要更改一个函数签名:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> MasterTable { get_spawn_table_for_depth(&RAWS.lock().unwrap(), map_depth) } }
如果您现在 cargo run
,那么怪物和物品的平衡性会好得多(在所有关卡中!)。
恶龙现身
这个关卡由一条黑龙统治。查看我们的 D&D 规则,这些都是可怕的巨大蜥蜴,拥有令人难以置信的体能,锋利的牙齿和爪子,以及可怕的酸性吐息,可以烧掉你骨头上的肉。有了这样的介绍,这条龙最好是相当可怕的!它也需要是可能被击败的,被一个等级达到 5 级的玩家。杀死龙真的应该给予一些非常惊人的奖励。如果你小心谨慎,也应该可以偷偷绕过龙!
在 spawns.json
中,我们将开始勾勒出龙的样子:
{
"name" : "Black Dragon",
"renderable": {
"glyph" : "D",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1
},
"blocks_tile" : true,
"vision_range" : 12,
"movement" : "static",
"attributes" : {
"might" : 13,
"fitness" : 13
},
"skills" : {
"Melee" : 18,
"Defense" : 16
},
"natural" : {
"armor_class" : 17,
"attacks" : [
{ "name" : "bite", "hit_bonus" : 4, "damage" : "1d10+2" },
{ "name" : "left_claw", "hit_bonus" : 2, "damage" : "1d10" },
{ "name" : "right_claw", "hit_bonus" : 2, "damage" : "1d10" }
]
},
"loot_table" : "Wyrms",
"faction" : "Wyrm",
"level" : 6,
"gold" : "10d6",
"abilities" : [
{ "spell" : "Acid Breath", "chance" : 0.2, "range" : 8.0, "min_range" : 2.0 }
]
},
我们还需要定义一个 Acid Breath 效果:
{
"name" : "Acid Breath",
"mana_cost" : 2,
"effects" : {
"ranged" : "6",
"damage" : "10",
"area_of_effect" : "3",
"particle" : "☼;#00FF00;400.0"
}
}
现在我们需要实际生成龙。我们不想将龙放入我们的生成表 - 那样会使它随机出现,并可能出现在错误的关卡中。这会破坏它作为 boss 的声誉!我们也不希望它被朋友包围 - 那样对玩家来说太难了(并且会分散对 boss 战的注意力)。
在 dwarf_fort_builder.rs
中,我们将为生成添加另一个层:
#![allow(unused)] fn main() { pub struct DragonSpawner {} impl MetaMapBuilder for DragonSpawner { fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.build(rng, build_data); } } impl DragonSpawner { #[allow(dead_code)] pub fn new() -> Box<DragonSpawner> { Box::new(DragonSpawner{}) } fn build(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { // 找到一个未被占用的中心位置 let seed_x = build_data.map.width / 2; let seed_y = build_data.map.height / 2; let mut available_floors : Vec<(usize, f32)> = Vec::new(); for (idx, tiletype) in build_data.map.tiles.iter().enumerate() { if crate::map::tile_walkable(*tiletype) { available_floors.push( ( idx, rltk::DistanceAlg::PythagorasSquared.distance2d( rltk::Point::new(idx as i32 % build_data.map.width, idx as i32 / build_data.map.width), rltk::Point::new(seed_x, seed_y) ) ) ); } } if available_floors.is_empty() { panic!("No valid floors to start on"); // 没有可供开始的有效地面 } available_floors.sort_by(|a,b| a.1.partial_cmp(&b.1).unwrap()); let start_x = available_floors[0].0 as i32 % build_data.map.width; let start_y = available_floors[0].0 as i32 / build_data.map.width; let dragon_pt = rltk::Point::new(start_x, start_y); // 移除龙周围 25 格范围内的所有生成物 let w = build_data.map.width as i32; build_data.spawn_list.retain(|spawn| { let spawn_pt = rltk::Point::new( spawn.0 as i32 % w, spawn.0 as i32 / w ); let distance = rltk::DistanceAlg::Pythagoras.distance2d(dragon_pt, spawn_pt); distance > 25.0 }); // 添加龙 let dragon_idx = build_data.map.xy_idx(start_x, start_y); build_data.spawn_list.push((dragon_idx, "Black Dragon".to_string())); } } }
这个函数非常直接,并且与我们之前编写的函数非常相似。我们找到地图中心附近的开放空间,然后移除所有距离中心点小于 25 格的怪物生成点(使怪物远离中心)。然后我们在中心生成黑龙。
让我们转到 main.rs
并临时更改 main
函数中的一行:
#![allow(unused)] fn main() { gs.generate_world_map(6, 0); }
这将使您从龙穴关卡开始(记住在我们完成后改回!),这样您就不必导航其他关卡来完成它。现在 cargo run
项目,使用作弊码(反斜杠
后跟 g
)启用 God Mode - 并探索关卡。看起来不错,但是龙太强大了,以至于杀死它需要很长时间 - 而且如果您查看伤害日志,玩家肯定会死!但是,通过一些练习 - 您可以使用法术和物品的组合来击倒龙。所以我们暂时就先这样了。
继续并将 main
函数改回去:
#![allow(unused)] fn main() { gs.generate_world_map(1, 0); }
龙不是很可怕
如果您玩游戏,龙非常致命。然而,它并没有太多的发自内心的冲击 - 它只是一个红色的 D
符号,有时会向您发射绿色的云雾。这是一个不错的想象力触发器,您甚至可以查看它的工具提示以了解 D
代表 Dragon
- 但似乎我们可以做得更好。此外,龙真的相当大 - 而且龙占据与绵羊相同的地图空间有点奇怪。
我们将首先添加一个新的组件来表示更大的实体:
#![allow(unused)] fn main() { #[derive(Component, ConvertSaveload, Clone)] pub struct TileSize { pub x: i32, pub y: i32, } }
像往常一样,我们不会忘记在 main.rs
和 saveload_system.rs
中注册它!我们还将允许您在 JSON 文件中为实体指定大小。在 raws/item_structs.rs
中,我们将扩展 Renderable
(记住,我们将其重用于其他类型):
#![allow(unused)] fn main() { #[derive(Deserialize, Debug)] pub struct Renderable { pub glyph: String, pub fg : String, pub bg : String, pub order: i32, pub x_size : Option<i32>, pub y_size : Option<i32> } }
我们将新字段设为可选 - 这样我们现有的代码就可以工作了。现在在 raws/rawmaster.rs
中,找到将 Renderable
组件添加到实体的 spawn_named_mob
函数部分(在我的源代码中大约在第 418 行)。如果指定了大小,我们需要添加一个 TileSize
组件:
#![allow(unused)] fn main() { // Renderable if let Some(renderable) = &mob_template.renderable { eb = eb.with(get_renderable_component(renderable)); if renderable.x_size.is_some() || renderable.y_size.is_some() { eb = eb.with(TileSize{ x : renderable.x_size.unwrap_or(1), y : renderable.y_size.unwrap_or(1) }); } } }
现在,我们将进入 spawns.json
并为龙添加额外的大小:
{
"name" : "Black Dragon",
"renderable": {
"glyph" : "D",
"fg" : "#FF0000",
"bg" : "#000000",
"order" : 1,
"x_size" : 2,
"y_size" : 2
},
...
在处理完内务管理后,我们需要能够将更大的实体渲染到地图上。打开 camera.rs
,我们将像这样修改渲染:
#![allow(unused)] fn main() { // 渲染实体 let positions = ecs.read_storage::<Position>(); let renderables = ecs.read_storage::<Renderable>(); let hidden = ecs.read_storage::<Hidden>(); let map = ecs.fetch::<Map>(); let sizes = ecs.read_storage::<TileSize>(); let entities = ecs.entities(); let mut data = (&positions, &renderables, &entities, !&hidden).join().collect::<Vec<_>>(); data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) ); for (pos, render, entity, _hidden) in data.iter() { if let Some(size) = sizes.get(*entity) { for cy in 0 .. size.y { for cx in 0 .. size.x { let tile_x = cx + pos.x; let tile_y = cy + pos.y; let idx = map.xy_idx(tile_x, tile_y); if map.visible_tiles[idx] { let entity_screen_x = (cx + pos.x) - min_x; let entity_screen_y = (cy + pos.y) - min_y; if entity_screen_x > 0 && entity_screen_x < map_width && entity_screen_y > 0 && entity_screen_y < map_height { ctx.set(entity_screen_x + 1, entity_screen_y + 1, render.fg, render.bg, render.glyph); } } } } } else { let idx = map.xy_idx(pos.x, pos.y); if map.visible_tiles[idx] { let entity_screen_x = pos.x - min_x; let entity_screen_y = pos.y - min_y; if entity_screen_x > 0 && entity_screen_x < map_width && entity_screen_y > 0 && entity_screen_y < map_height { ctx.set(entity_screen_x + 1, entity_screen_y + 1, render.fg, render.bg, render.glyph); } } } } }
这是如何工作的?我们检查我们正在渲染的实体是否具有 TileSize
组件,使用 if let
语法进行匹配赋值。如果它有,我们为其指定的大小单独渲染每个瓦片。如果它没有,我们完全像以前一样渲染。请注意,我们正在对每个瓦片进行边界和可见性检查;这不是最快的方法,但确实保证了如果您可以看到龙的一部分,它将被渲染。
如果您现在 cargo run
,您会发现自己面对的是一条更大的龙:
选择龙
如果您实际上与龙交战,就会出现很多问题:
- 对于远程攻击,您只能以龙的左上角瓦片为目标。这包括范围效果。
- 近战也仅影响龙的左上角瓦片。
- 您实际上可以穿过龙的其他瓦片。
- 龙可以穿过地形并仍然沿着狭窄的走廊行走。它可能很擅长折叠翅膀!
幸运的是,我们可以通过 map_indexing_system
解决很多问题。系统需要扩展以考虑多瓦片实体,并为实体占用的每个瓦片存储一个条目:
#![allow(unused)] fn main() { use specs::prelude::*; use super::{Map, Position, BlocksTile, Pools, spatial, TileSize}; pub struct MapIndexingSystem {} impl<'a> System<'a> for MapIndexingSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Map>, ReadStorage<'a, Position>, ReadStorage<'a, BlocksTile>, ReadStorage<'a, Pools>, ReadStorage<'a, TileSize>, Entities<'a>,); fn run(&mut self, data : Self::SystemData) { let (map, position, blockers, pools, sizes, entities) = data; spatial::clear(); spatial::populate_blocked_from_map(&*map); for (entity, position) in (&entities, &position).join() { let mut alive = true; if let Some(pools) = pools.get(entity) { if pools.hit_points.current < 1 { alive = false; } } if alive { if let Some(size) = sizes.get(entity) { // 多瓦片 for y in position.y .. position.y + size.y { for x in position.x .. position.x + size.x { if x > 0 && x < map.width-1 && y > 0 && y < map.height-1 { let idx = map.xy_idx(x, y); spatial::index_entity(entity, idx, blockers.get(entity).is_some()); } } } } else { // 单瓦片 let idx = map.xy_idx(position.x, position.y); spatial::index_entity(entity, idx, blockers.get(entity).is_some()); } } } } } }
这解决了几个问题:您现在可以攻击龙的任何部分,龙的所有身体都阻止其他人穿过它,并且远程目标定位可以针对其任何瓦片。然而,工具提示仍然顽固地不起作用 - 您只能从龙的左上角瓦片获取信息。幸运的是,很容易切换工具提示系统以使用 map.tile_content
结构,而不是重复迭代位置。它可能也表现更好。在 gui.rs
中,将函数的开头替换为:
#![allow(unused)] fn main() { fn draw_tooltips(ecs: &World, ctx : &mut Rltk) { use rltk::to_cp437; use rltk::Algorithm2D; let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx); let map = ecs.fetch::<Map>(); let hidden = ecs.read_storage::<Hidden>(); let attributes = ecs.read_storage::<Attributes>(); let pools = ecs.read_storage::<Pools>(); let mouse_pos = ctx.mouse_pos(); let mut mouse_map_pos = mouse_pos; mouse_map_pos.0 += min_x - 1; mouse_map_pos.1 += min_y - 1; if mouse_pos.0 < 1 || mouse_pos.0 > 49 || mouse_pos.1 < 1 || mouse_pos.1 > 40 { return; } if mouse_map_pos.0 >= map.width-1 || mouse_map_pos.1 >= map.height-1 || mouse_map_pos.0 < 1 || mouse_map_pos.1 < 1 { return; } if !map.in_bounds(rltk::Point::new(mouse_map_pos.0, mouse_map_pos.1)) { return; } let mouse_idx = map.xy_idx(mouse_map_pos.0, mouse_map_pos.1); if !map.visible_tiles[mouse_idx] { return; } let mut tip_boxes : Vec<Tooltip> = Vec::new(); for entity in map.tile_content[mouse_idx].iter().filter(|e| hidden.get(**e).is_none()) { ... }
现在您可以使用工具提示来识别龙,并以它的任何部分为目标。为了展示代码的通用性,这是一张带有真正巨大的龙的屏幕截图:
您可能注意到龙死得非常容易。发生了什么?
- 龙对自己的吐息武器免疫,因此处于吐息半径内会对这可怜的野兽造成伤害。
- 范围效果系统意味着龙被反复击中 - 多个瓦片在半径范围内,因此对于每个瓦片 - 龙都受到了伤害。这并非完全不现实(您会期望火球术击中大型目标时会击中更大的表面积),但这绝对是一个意想不到的后果!范围效果毒药或网也会在可怜的受害者身上堆叠每个瓦片一个状态效果。
范围效果是否通常会击中施法者是一个有趣的问题;它在 火球术 上实现了良好的平衡(并且是 D&D 的一句老话 - 小心不要击中自己),但它绝对会导致意想不到的效果。
幸运的是,我们可以通过简单地更改 effects/damage.rs
来解决第一部分 - 自残:
#![allow(unused)] fn main() { pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) { let mut pools = ecs.write_storage::<Pools>(); if let Some(pool) = pools.get_mut(target) { if !pool.god_mode { if let Some(creator) = damage.creator { if creator == target { return; } } ... }
范围效果法术不再消灭施法者,但仍然可以击中友方。这是一个不错的折衷方案!我们还将在 effects
系统中添加去重功能。无论如何,这可能是一个好主意。打开 effects/mod.rs
,我们将开始。首先,我们需要包含 HashSet
作为导入的类型:
#![allow(unused)] fn main() { use std::collections::{HashSet, VecDeque}; }
接下来,我们将向 EffectSpawner
类型添加一个 dedupe
字段:
#![allow(unused)] fn main() { #[derive(Debug)] pub struct EffectSpawner { pub creator : Option<Entity>, pub effect_type : EffectType, pub targets : Targets, dedupe : HashSet<Entity> } }
并修改 add_effect
函数以包含一个:
#![allow(unused)] fn main() { pub fn add_effect(creator : Option<Entity>, effect_type: EffectType, targets : Targets) { EFFECT_QUEUE .lock() .unwrap() .push_back(EffectSpawner{ creator, effect_type, targets, dedupe : HashSet::new() }); } }
接下来,我们需要修改许多位置以使引用的效果可变 - 就像它可以被更改一样:
#![allow(unused)] fn main() { pub fn run_effects_queue(ecs : &mut World) { loop { let effect : Option<EffectSpawner> = EFFECT_QUEUE.lock().unwrap().pop_front(); if let Some(mut effect) = effect { target_applicator(ecs, &mut effect); } else { break; } } } fn target_applicator(ecs : &mut World, effect : &mut EffectSpawner) { if let EffectType::ItemUse{item} = effect.effect_type { triggers::item_trigger(effect.creator, item, &effect.targets, ecs); } else if let EffectType::SpellUse{spell} = effect.effect_type { triggers::spell_trigger(effect.creator, spell, &effect.targets, ecs); } else if let EffectType::TriggerFire{trigger} = effect.effect_type { triggers::trigger(effect.creator, trigger, &effect.targets, ecs); } else { match &effect.targets.clone() { Targets::Tile{tile_idx} => affect_tile(ecs, effect, *tile_idx), Targets::Tiles{tiles} => tiles.iter().for_each(|tile_idx| affect_tile(ecs, effect, *tile_idx)), Targets::Single{target} => affect_entity(ecs, effect, *target), Targets::TargetList{targets} => targets.iter().for_each(|entity| affect_entity(ecs, effect, *entity)), } } } fn affect_tile(ecs: &mut World, effect: &mut EffectSpawner, tile_idx : i32) { ... } }
最后,让我们将重复预防添加到 affect_entity
:
#![allow(unused)] fn main() { fn affect_entity(ecs: &mut World, effect: &mut EffectSpawner, target: Entity) { if effect.dedupe.contains(&target) { return; } effect.dedupe.insert(target); match &effect.effect_type { EffectType::Damage{..} => damage::inflict_damage(ecs, effect, target), EffectType::EntityDeath => damage::death(ecs, effect, target), EffectType::Bloodstain{..} => if let Some(pos) = entity_position(ecs, target) { damage::bloodstain(ecs, pos) }, EffectType::Particle{..} => if let Some(pos) = entity_position(ecs, target) { particles::particle_to_tile(ecs, pos, &effect) }, EffectType::WellFed => hunger::well_fed(ecs, effect, target), EffectType::Healing{..} => damage::heal_damage(ecs, effect, target), EffectType::Mana{..} => damage::restore_mana(ecs, effect, target), EffectType::Confusion{..} => damage::add_confusion(ecs, effect, target), EffectType::TeleportTo{..} => movement::apply_teleport(ecs, effect, target), EffectType::AttributeEffect{..} => damage::attribute_effect(ecs, effect, target), EffectType::Slow{..} => damage::slow(ecs, effect, target), EffectType::DamageOverTime{..} => damage::damage_over_time(ecs, effect, target), _ => {} } } }
如果您现在 cargo run
,龙将完全不会影响自己。如果您向龙发射火球术(我临时修改了 spawner.rs
以便开始时拥有 火焰球法杖 来进行测试!),它只会对龙产生一次影响。太棒了!
让龙从任何瓦片攻击
您可能注意到的另一个问题是,龙只能从其“头部”(左上角瓦片)攻击您。我喜欢将龙想象成具有猫一样的敏捷性(我倾向于认为它们通常很像猫!),因此这行不通!我们将从一个辅助函数开始。打开历史悠久的 rect.rs
(自从开始以来我们就没有碰过它!),我们将向其中添加一个新函数:
#![allow(unused)] fn main() { use std::collections::HashSet; ... pub fn get_all_tiles(&self) -> HashSet<(i32,i32)> { let mut result = HashSet::new(); for y in self.y1 .. self.y2 { for x in self.x1 .. self.x2 { result.insert((x,y)); } } result } }
这将返回矩形内瓦片的 HashSet
。非常简单,并希望优化成一个非常快速的函数!现在我们进入 ai/adjacent_ai_system.rs
。我们将修改系统以也查询 TileSize
:
#![allow(unused)] fn main() { impl<'a> System<'a> for AdjacentAI { #[allow(clippy::type_complexity)] type SystemData = ( WriteStorage<'a, MyTurn>, ReadStorage<'a, Faction>, ReadStorage<'a, Position>, ReadExpect<'a, Map>, WriteStorage<'a, WantsToMelee>, Entities<'a>, ReadExpect<'a, Entity>, ReadStorage<'a, TileSize> ); fn run(&mut self, data : Self::SystemData) { let (mut turns, factions, positions, map, mut want_melee, entities, player, sizes) = data; }
然后我们将检查是否存在不规则大小(如果没有,则使用旧代码) - 否则进行一些矩形数学运算以找到相邻的瓦片:
#![allow(unused)] fn main() { fn run(&mut self, data : Self::SystemData) { let (mut turns, factions, positions, map, mut want_melee, entities, player, sizes) = data; let mut turn_done : Vec<Entity> = Vec::new(); for (entity, _turn, my_faction, pos) in (&entities, &turns, &factions, &positions).join() { if entity != *player { let mut reactions : Vec<(Entity, Reaction)> = Vec::new(); let idx = map.xy_idx(pos.x, pos.y); let w = map.width; let h = map.height; if let Some(size) = sizes.get(entity) { use crate::rect::Rect; let mob_rect = Rect::new(pos.x, pos.y, size.x, size.y).get_all_tiles(); let parent_rect = Rect::new(pos.x -1, pos.y -1, size.x+2, size.y + 2); parent_rect.get_all_tiles().iter().filter(|t| !mob_rect.contains(t)).for_each(|t| { if t.0 > 0 && t.0 < w-1 && t.1 > 0 && t.1 < h-1 { let target_idx = map.xy_idx(t.0, t.1); evaluate(target_idx, &map, &factions, &my_faction.name, &mut reactions); } }); } else { // 为每个方向的相邻位置添加可能的反应 if pos.x > 0 { evaluate(idx-1, &map, &factions, &my_faction.name, &mut reactions); } if pos.x < w-1 { evaluate(idx+1, &map, &factions, &my_faction.name, &mut reactions); } if pos.y > 0 { evaluate(idx-w as usize, &map, &factions, &my_faction.name, &mut reactions); } if pos.y < h-1 { evaluate(idx+w as usize, &map, &factions, &my_faction.name, &mut reactions); } if pos.y > 0 && pos.x > 0 { evaluate((idx-w as usize)-1, &map, &factions, &my_faction.name, &mut reactions); } if pos.y > 0 && pos.x < w-1 { evaluate((idx-w as usize)+1, &map, &factions, &my_faction.name, &mut reactions); } if pos.y < h-1 && pos.x > 0 { evaluate((idx+w as usize)-1, &map, &factions, &my_faction.name, &mut reactions); } if pos.y < h-1 && pos.x < w-1 { evaluate((idx+w as usize)+1, &map, &factions, &my_faction.name, &mut reactions); } } ... }
逐步分析:
- 我们从与之前相同的设置开始。
- 我们使用
if let
来获取瓦片大小(如果有)。 - 我们设置一个
mob_rect
,它等于怪物的位置和尺寸,并获取它覆盖的瓦片的 Set。 - 我们设置一个
parent_rect
,它在所有方向上都大一格瓦片。 - 我们调用
parent_rect.get_all_tiles()
并将其转换为迭代器。然后我们filter
它以仅包含不在mob_rect
中的瓦片 - 因此我们拥有所有相邻的瓦片。 - 然后我们在结果瓦片集上使用
for_each
,确保瓦片在地图范围内,并将它们添加到调用evaluate
的位置。
如果您现在 cargo run
,当您在龙周围行走时,龙可以攻击您。
裁剪龙的翅膀
另一个问题是,即使龙不适合,它也可以跟随您进入狭窄的走廊。大型实体的寻路在游戏中通常存在问题;《矮人要塞》最终为货车建立了一个糟糕的完全独立的系统!让我们希望我们可以做得更好。我们的移动系统在很大程度上依赖于地图内部的 blocked
结构,因此我们需要一种为大于一个瓦片的实体添加阻塞信息的方法。在 map/mod.rs
中,我们添加以下内容:
#![allow(unused)] fn main() { pub fn populate_blocked_multi(&mut self, width : i32, height : i32) { self.populate_blocked(); for y in 1 .. self.height-1 { for x in 1 .. self.width - 1 { let idx = self.xy_idx(x, y); if !crate::spatial::is_blocked(idx) { for cy in 0..height { for cx in 0..width { let tx = x + cx; let ty = y + cy; if tx < self.width-1 && ty < self.height-1 { let tidx = self.xy_idx(tx, ty); if crate::spatial::is_blocked(tidx) { crate::spatial::set_blocked(idx, true); } } else { crate::spatial::set_blocked(idx, true); } } } } } } } }
我要警告不要使用那么多嵌套循环,但至少它有转义子句!那么这是做什么的呢:
- 它首先使用现有的
populate_blocked
函数为所有瓦片构建blocked
信息。 - 然后它迭代地图,如果一个瓦片没有被阻塞:
- 它迭代实体大小范围内的每个瓦片,添加到当前坐标。
- 如果这些瓦片中的任何一个被阻塞,它也会将要检查的瓦片设置为阻塞。
因此,最终结果是您获得了一个大型实体可以站立的位置的地图。现在我们需要将其插入 ai/chase_ai_system.rs
:
#![allow(unused)] fn main() { ... turn_done.push(entity); let target_pos = targets[&entity]; let path; if let Some(size) = sizes.get(entity) { let mut map_copy = map.clone(); map_copy.populate_blocked_multi(size.x, size.y); path = rltk::a_star_search( map_copy.xy_idx(pos.x, pos.y) as i32, map_copy.xy_idx(target_pos.0, target_pos.1) as i32, &mut map_copy ); } else { path = rltk::a_star_search( map.xy_idx(pos.x, pos.y) as i32, map.xy_idx(target_pos.0, target_pos.1) as i32, &mut *map ); } if path.success && path.steps.len()>1 && path.steps.len()<15 { ... }
因此,我们已将 path
的值更改为与 1x1 实体之前的代码相同,但对于大型实体,则获取地图的 克隆 并使用新的 populate_blocked_multi
函数来添加适合此大小生物的阻塞。
如果您现在 cargo run
,您可以通过狭窄的通道逃脱龙的追捕。
为什么要付出这么多努力?
那么,当我们本可以为黑龙特殊处理时,为什么我们要花费这么多时间使不规则大小的对象如此通用地工作?这是为了让我们以后可以制作更多的大型物体。:-)
友情提示:总是很想为所有事物都这样做,但请记住 YAGNI
规则:You Ain't Gonna Need It
(你不会需要它)。如果您没有充分的理由来实现某个功能,请坚持到您需要它或它有意义为止!(有趣的是:我第一次听到这个规则是一位网名为 TANSAAFL
的朋友说的。我花了很长时间才意识到他的意思是“天下没有免费的午餐。”)
担心平衡/游戏测试
现在到了困难的部分。现在是玩几次 1 到 6 级关卡的好时机,看看您能走多远,并看看您遇到了什么问题。以下是我注意到的一些事项:
- 我遇到了 鹿 比预期更烦人的情况,所以目前我已将它们从生成表中删除。
- 老鼠 实际上无法伤害您!将它们的威力更改为 7 会带来最小的惩罚,但会使它们偶尔造成一些伤害。
- 治疗药水需要更频繁地生成!我将其生成权重更改为 15。我遇到了几次我真的需要紧急治疗的情况。
- 游戏有点太难了;您真的要听天由命。您可能会做得很好,但一个掷骰子好的强盗会无情地屠杀您 - 即使在您设法穿上一些盔甲并升级之后!我决定做两件事来纠正这一点:
我通过更改 gamessytem.rs
中的 player_hp_per_level
和 player_hp_at_level
,使玩家每级获得 20 点生命值而不是 10 点:
#![allow(unused)] fn main() { pub fn player_hp_per_level(fitness: i32) -> i32 { 15 + attr_bonus(fitness) } pub fn player_hp_at_level(fitness:i32, level:i32) -> i32 { 15 + (player_hp_per_level(fitness) * level) } }
在 effects/damage.rs
中,当我们处理升级时,我给了玩家更多升级的理由!随机属性和所有技能都会提高。因此,升级使您更快、更强壮且更具破坏力。它也使您更难被击中。这是代码:
#![allow(unused)] fn main() { if xp_gain != 0 || gold_gain != 0.0 { let mut log = ecs.fetch_mut::<GameLog>(); let mut player_stats = pools.get_mut(source).unwrap(); let mut player_attributes = attributes.get_mut(source).unwrap(); player_stats.xp += xp_gain; player_stats.gold += gold_gain; if player_stats.xp >= player_stats.level * 1000 { // 我们升级了! player_stats.level += 1; log.entries.push(format!("Congratulations, you are now level {}", player_stats.level)); // 恭喜,您现在是 {} 级了 // 提升一个随机属性 let mut rng = ecs.fetch_mut::<rltk::RandomNumberGenerator>(); let attr_to_boost = rng.roll_dice(1, 4); match attr_to_boost { 1 => { player_attributes.might.base += 1; log.entries.push("You feel stronger!".to_string()); // 您感觉更强壮了! } 2 => { player_attributes.fitness.base += 1; log.entries.push("You feel healthier!".to_string()); // 您感觉更健康了! } 3 => { player_attributes.quickness.base += 1; log.entries.push("You feel quicker!".to_string()); // 您感觉更快了! } _ => { player_attributes.intelligence.base += 1; log.entries.push("You feel smarter!".to_string()); // 您感觉更聪明了! } } // 提升所有技能 let mut skills = ecs.write_storage::<Skills>(); let player_skills = skills.get_mut(*ecs.fetch::<Entity>()).unwrap(); for sk in player_skills.skills.iter_mut() { *sk.1 += 1; } ecs.write_storage::<EquipmentChanged>() .insert( *ecs.fetch::<Entity>(), EquipmentChanged{}) .expect("Insert Failed"); // 插入失败 player_stats.hit_points.max = player_hp_at_level( player_attributes.fitness.base + player_attributes.fitness.modifiers, player_stats.level ); player_stats.hit_points.current = player_stats.hit_points.max; player_stats.mana.max = mana_at_level( player_attributes.intelligence.base + player_attributes.intelligence.modifiers, player_stats.level ); player_stats.mana.current = player_stats.mana.max; let player_pos = ecs.fetch::<rltk::Point>(); let map = ecs.fetch::<Map>(); for i in 0..10 { if player_pos.y - i > 1 { add_effect(None, EffectType::Particle{ glyph: rltk::to_cp437('░'), fg : rltk::RGB::named(rltk::GOLD), bg : rltk::RGB::named(rltk::BLACK), lifespan: 400.0 }, Targets::Tile{ tile_idx : map.xy_idx(player_pos.x, player_pos.y - i) as i32 } ); } } } } }
在这些更改之后再次玩游戏,游戏变得容易得多 - 而且感觉我可以取得进展(但仍然面临真正的死亡风险)。龙仍然击败了我,但是这是一场非常接近的战斗 - 我几乎赢了!所以我又玩了几次,一旦我找到了一种奖励我的法术和物品使用策略,我就获得了胜利。太棒了 - 这就是 roguelike 应该有的游戏类型!
我还发现在早期关卡中,如果我不注意就会死 - 但如果我专心致志,通常会取得胜利。
总结
因此,在本章中,我们构建了一个龙穴 - 并用一条邪恶的龙填充了它。它几乎可以被击败,但您真的需要动脑筋。
...
本章的源代码可以在这里找到
在您的浏览器中使用 Web Assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。