门
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
门和角落,那是他们抓住你的地方。 如果我们要让米勒(来自《太空无垠》- 可能是目前我最喜欢的科幻小说系列)的警告成真 - 在游戏中有门将是个好主意。 门是地下城探索的必备元素! 我们等待了这么久才实现它们,是为了确保我们有放置它们的合适位置。
门也是实体
我们将从简单的装饰性门开始,这些门根本不做任何事情。 这将使我们能够适当地放置它们,然后我们可以实现一些与门相关的功能。 距离我们上次添加实体类型已经有一段时间了; 幸运的是,我们现有的 components
中拥有装饰性门所需的一切。 打开 spawner.rs
,并重新熟悉它! 然后我们将添加一个门生成器函数:
#![allow(unused)] fn main() { fn door(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('+'), fg: RGB::named(rltk::CHOCOLATE), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Door".to_string() }) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
所以我们仅用于装饰的门非常简单:它有一个字形(glyph)(在许多 roguelike 游戏中,+
是传统的),是棕色的,并且具有 Name
和 Position
。 这就是让它们出现在地图上所需的全部! 我们还将修改 spawn_entity
以了解在给定要生成的 Door 时该怎么做:
#![allow(unused)] fn main() { match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y), "Orc" => orc(ecs, x, y), "Health Potion" => health_potion(ecs, x, y), "Fireball Scroll" => fireball_scroll(ecs, x, y), "Confusion Scroll" => confusion_scroll(ecs, x, y), "Magic Missile Scroll" => magic_missile_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), _ => {} } }
我们不会将门添加到生成表(spawn tables)中; 让它们随机出现在房间里是没有意义的!
放置门
我们将创建一个新的 builder(毕竟我们仍然在地图部分!)它可以放置门。 因此,在 map_builders
中,创建一个新文件:door_placement.rs
:
#![allow(unused)] fn main() { use super::{MetaMapBuilder, BuilderMap }; use rltk::RandomNumberGenerator; pub struct DoorPlacement {} impl MetaMapBuilder for DoorPlacement { #[allow(dead_code)] fn build_map(&mut self, rng: &mut rltk::RandomNumberGenerator, build_data : &mut BuilderMap) { self.doors(rng, build_data); } } impl DoorPlacement { #[allow(dead_code)] pub fn new() -> Box<DoorPlacement> { Box::new(DoorPlacement{ }) } fn doors(&mut self, _rng : &mut RandomNumberGenerator, _build_data : &mut BuilderMap) { } } }
这是一个元 builder 的空骨架。 让我们首先处理最简单的情况:当我们有走廊数据时,它提供了一个门可能适合的位置的蓝图。 我们将从一个新函数 door_possible
开始:
#![allow(unused)] fn main() { fn door_possible(&self, build_data : &mut BuilderMap, idx : usize) -> bool { let x = idx % build_data.map.width as usize; let y = idx / build_data.map.width as usize; // 检查东西方向门的可能性 if build_data.map.tiles[idx] == TileType::Floor && (x > 1 && build_data.map.tiles[idx-1] == TileType::Floor) && (x < build_data.map.width-2 && build_data.map.tiles[idx+1] == TileType::Floor) && (y > 1 && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Wall) && (y < build_data.map.height-2 && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Wall) { return true; } // 检查南北方向门的可能性 if build_data.map.tiles[idx] == TileType::Floor && (x > 1 && build_data.map.tiles[idx-1] == TileType::Wall) && (x < build_data.map.width-2 && build_data.map.tiles[idx+1] == TileType::Wall) && (y > 1 && build_data.map.tiles[idx - build_data.map.width as usize] == TileType::Floor) && (y < build_data.map.height-2 && build_data.map.tiles[idx + build_data.map.width as usize] == TileType::Floor) { return true; } false } }
实际上只有两种门有意义的位置:东西方向开放且南北方向被阻挡,反之亦然。 我们不希望门出现在开放区域。 因此,此函数检查这些条件,如果门是可能的,则返回 true
,否则返回 false
。 现在我们扩展 doors
函数以扫描走廊并在其开头放置门:
#![allow(unused)] fn main() { fn doors(&mut self, _rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(halls_original) = &build_data.corridors { let halls = halls_original.clone(); // 为了避免嵌套借用 for hall in halls.iter() { if hall.len() > 2 { // 我们对微小的走廊不感兴趣 if self.door_possible(build_data, hall[0]) { build_data.spawn_list.push((hall[0], "Door".to_string())); } } } } } }
我们首先检查是否有走廊信息可以使用。 如果有,我们复制一份(为了让借用检查器高兴 - 否则我们将对 halls
进行两次借用)并迭代它。 每个条目都是一个走廊 - 构成该走廊的瓦片(tile)向量。 我们只对长度超过 2 个条目的走廊感兴趣 - 以避免连接门的非常短的走廊。 因此,如果它足够长 - 我们检查在走廊索引 0
处放置门是否合理; 如果合理,我们将其添加到生成列表(spawn list)中。
我们将快速再次修改 random_builder
以创建一个可能生成门的情况:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(SimpleMapBuilder::new()); builder.with(RoomDrawer::new()); builder.with(RoomSorter::new(RoomSort::LEFTMOST)); builder.with(StraightLineCorridors::new()); builder.with(RoomBasedSpawner::new()); builder.with(CorridorSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); builder.with(DoorPlacement::new()); builder }
我们 cargo run
运行项目,瞧 - 门出现了:
.
其他设计怎么样?
当然可以逐瓦片扫描其他地图,看看是否有门出现的可能性。 让我们这样做:
#![allow(unused)] fn main() { if let Some(halls_original) = &build_data.corridors { let halls = halls_original.clone(); // 为了避免嵌套借用 for hall in halls.iter() { if hall.len() > 2 { // 我们对微小的走廊不感兴趣 if self.door_possible(build_data, hall[0]) { build_data.spawn_list.push((hall[0], "Door".to_string())); } } } } else { // 没有走廊 - 扫描可能的位置 let tiles = build_data.map.tiles.clone(); for (i, tile) in tiles.iter().enumerate() { if *tile == TileType::Floor && self.door_possible(build_data, i) { build_data.spawn_list.push((i, "Door".to_string())); } } } } }
修改你的 random_builder
以使用没有走廊的地图:
#![allow(unused)] fn main() { let mut builder = BuilderChain::new(new_depth); builder.start_with(BspInteriorBuilder::new()); builder.with(DoorPlacement::new()); builder.with(RoomBasedSpawner::new()); builder.with(RoomBasedStairs::new()); builder.with(RoomBasedStartingPosition::new()); builder }
你可以 cargo run
运行项目并看到门:
.
效果相当好!
恢复我们的随机函数
我们将 random_builder
恢复到原来的样子,但有一个更改:我们将添加一个门生成器作为最后一步:
#![allow(unused)] fn main() { pub fn random_builder(new_depth: i32, rng: &mut rltk::RandomNumberGenerator) -> BuilderChain { let mut builder = BuilderChain::new(new_depth); let type_roll = rng.roll_dice(1, 2); match type_roll { 1 => random_room_builder(rng, &mut builder), _ => random_shape_builder(rng, &mut builder) } if rng.roll_dice(1, 3)==1 { builder.with(WaveformCollapseBuilder::new()); } if rng.roll_dice(1, 20)==1 { builder.with(PrefabBuilder::sectional(prefab_builder::prefab_sections::UNDERGROUND_FORT)); } builder.with(DoorPlacement::new()); builder.with(PrefabBuilder::vaults()); builder } }
请注意,我们在添加 vault 之前 添加了它; 这是故意的 - vault 有机会生成并删除任何会干扰它的门。
让门发挥作用
门有几个属性:当关闭时,它们会阻挡移动和视野。 它们可以被打开(可以选择性地需要解锁,但我们现在不打算这样做),此时你可以很好地看穿它们。
让我们从“勾勒出”(suggesting!)一些新的组件开始。 在 spawner.rs
中:
#![allow(unused)] fn main() { fn door(ecs: &mut World, x: i32, y: i32) { ecs.create_entity() .with(Position{ x, y }) .with(Renderable{ glyph: rltk::to_cp437('+'), fg: RGB::named(rltk::CHOCOLATE), bg: RGB::named(rltk::BLACK), render_order: 2 }) .with(Name{ name : "Door".to_string() }) .with(BlocksTile{}) .with(BlocksVisibility{}) .with(Door{open: false}) .marked::<SimpleMarker<SerializeMe>>() .build(); } }
这里有两种新的组件类型!
BlocksVisibility
将实现其名称所示的功能 - 阻止你(和怪物)看穿它。 将其作为组件而不是特殊情况处理是很好的,因为现在你可以使任何东西阻挡视野。 一个非常大的宝箱,一个巨人,甚至是一堵移动的墙 - 能够阻止看穿它们是有意义的。Door
- 表示它是一扇门,并且需要自己的处理方式。
打开 components.rs
,我们将创建这些新组件:
#![allow(unused)] fn main() { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct BlocksVisibility {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Door { pub open: bool } }
与所有组件一样,不要忘记在 main
和 saveload_system.rs
中注册它们。
扩展视野系统以处理阻挡您视线的实体
由于视野(field of view)由 RLTK 处理,而 RLTK 依赖于 Map
trait - 我们需要扩展我们的地图类以处理这个概念。 添加一个新字段:
#![allow(unused)] fn main() { #[derive(Default, Serialize, Deserialize, Clone)] pub struct Map { pub tiles : Vec<TileType>, pub width : i32, pub height : i32, pub revealed_tiles : Vec<bool>, pub visible_tiles : Vec<bool>, pub blocked : Vec<bool>, pub depth : i32, pub bloodstains : HashSet<usize>, pub view_blocked : HashSet<usize>, #[serde(skip_serializing)] #[serde(skip_deserializing)] pub tile_content : Vec<Vec<Entity>> } }
并更新构造函数,以免忘记:
#![allow(unused)] fn main() { pub fn new(new_depth : i32) -> Map { Map{ tiles : vec![TileType::Wall; MAPCOUNT], width : MAPWIDTH as i32, height: MAPHEIGHT as i32, revealed_tiles : vec![false; MAPCOUNT], visible_tiles : vec![false; MAPCOUNT], blocked : vec![false; MAPCOUNT], tile_content : vec![Vec::new(); MAPCOUNT], depth: new_depth, bloodstains: HashSet::new(), view_blocked : HashSet::new() } } }
现在我们将更新 is_opaque
函数(视野(field-of-view)使用),以包含对其的检查:
#![allow(unused)] fn main() { fn is_opaque(&self, idx:i32) -> bool { let idx_u = idx as usize; self.tiles[idx_u] == TileType::Wall || self.view_blocked.contains(&idx_u) } }
我们还需要访问 visibility_system.rs
以填充此数据。 我们需要扩展系统的数据以检索更多内容:
#![allow(unused)] fn main() { type SystemData = ( WriteExpect<'a, Map>, Entities<'a>, WriteStorage<'a, Viewshed>, ReadStorage<'a, Position>, ReadStorage<'a, Player>, WriteStorage<'a, Hidden>, WriteExpect<'a, rltk::RandomNumberGenerator>, WriteExpect<'a, GameLog>, ReadStorage<'a, Name>, ReadStorage<'a, BlocksVisibility>); fn run(&mut self, data : Self::SystemData) { let (mut map, entities, mut viewshed, pos, player, mut hidden, mut rng, mut log, names, blocks_visibility) = data; ... }
紧随其后,我们将循环遍历所有阻挡视野的实体,并在 view_blocked
HashSet
中设置它们的索引:
#![allow(unused)] fn main() { map.view_blocked.clear(); for (block_pos, _block) in (&pos, &blocks_visibility).join() { let idx = map.xy_idx(block_pos.x, block_pos.y); map.view_blocked.insert(idx); } }
如果你现在 cargo run
运行项目,你会看到门现在阻挡了视线:
.
处理门
撞到一扇关着的门应该打开它,然后你就可以自由通过(我们可以添加 open
和 close
命令 - 也许我们稍后会这样做 - 但现在让我们保持简单)。 打开 player.rs
,我们将向 try_move_player
添加功能:
#![allow(unused)] fn main() { ... let mut doors = ecs.write_storage::<Door>(); let mut blocks_visibility = ecs.write_storage::<BlocksVisibility>(); let mut blocks_movement = ecs.write_storage::<BlocksTile>(); let mut renderables = ecs.write_storage::<Renderable>(); for (entity, _player, pos, viewshed) in (&entities, &players, &mut positions, &mut viewsheds).join() { if pos.x + delta_x < 1 || pos.x + delta_x > map.width-1 || pos.y + delta_y < 1 || pos.y + delta_y > map.height-1 { return RunState::AwaitingInput; } let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); for potential_target in map.tile_content[destination_idx].iter() { 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; } let door = doors.get_mut(*potential_target); if let Some(door) = door { door.open = true; blocks_visibility.remove(*potential_target); blocks_movement.remove(*potential_target); let glyph = renderables.get_mut(*potential_target).unwrap(); glyph.glyph = rltk::to_cp437('/'); viewshed.dirty = true; } } ... }
让我们逐步了解它:
- 我们获得对
Door
、BlocksVisibility
、BlocksTile
和Renderable
存储的写入访问权限。 - 我们迭代移动瓦片中的潜在目标,像以前一样处理近战。
- 我们还检查潜在目标是否是一扇门。 如果是,则:
- 将门的
open
变量设置为true
。 - 删除
BlocksVisibility
条目 - 现在你可以看穿它了(怪物也可以!)。 - 删除
BlocksTile
条目 - 现在你可以穿过它了(所有人都可以!)。 - 更新字形(glyph)以显示打开的门口。
- 我们将视野(viewshed)标记为脏的(dirty),以便现在显示你可以通过门看到的东西。
- 将门的
如果你现在 cargo run
运行项目,你将获得所需的功能:
.
门太多了!
在非走廊地图上,在测试门放置时存在一个小问题:到处都是门。 让我们降低门放置的频率。 我们只需添加一点随机性:
#![allow(unused)] fn main() { fn doors(&mut self, rng : &mut RandomNumberGenerator, build_data : &mut BuilderMap) { if let Some(halls_original) = &build_data.corridors { let halls = halls_original.clone(); // 为了避免嵌套借用 for hall in halls.iter() { if hall.len() > 2 { // 我们对微小的走廊不感兴趣 if self.door_possible(build_data, hall[0]) { build_data.spawn_list.push((hall[0], "Door".to_string())); } } } } else { // 没有走廊 - 扫描可能的位置 let tiles = build_data.map.tiles.clone(); for (i, tile) in tiles.iter().enumerate() { if *tile == TileType::Floor && self.door_possible(build_data, i) && rng.roll_dice(1,3)==1 { build_data.spawn_list.push((i, "Door".to_string())); } } } } }
这给出了任何可能的门放置产生门的 1/3 的机会。 从玩游戏的角度来看,这感觉差不多是对的。 它可能不适合你 - 所以你可以更改它! 你甚至可能想把它做成一个参数。
门在其他实体之上
有时,门会生成在另一个实体之上。 这很少见,但可能会发生。 让我们防止这个问题发生。 我们可以通过快速扫描 door_possible
中的生成列表(spawn list)来解决这个问题:
#![allow(unused)] fn main() { fn door_possible(&self, build_data : &mut BuilderMap, idx : usize) -> bool { let mut blocked = false; for spawn in build_data.spawn_list.iter() { if spawn.0 == idx { blocked = true; } } if blocked { return false; } ... }
如果速度成为一个问题,这将很容易加速(创建一个已占用瓦片的快速 HashSet
,并查询它而不是整个列表) - 但我们实际上没有任何性能问题,并且地图构建在主循环之外运行(所以它是每个级别一次,而不是每帧) - 所以你很可能不需要它。
附录:修复 WFC
在我们的 random_builder
中,我们犯了一个错误! 波函数坍缩(Wave Function Collapse)改变了地图的性质,应该调整生成点(spawn)、入口点和出口点。 这是正确的代码:
#![allow(unused)] fn main() { if rng.roll_dice(1, 3)==1 { builder.with(WaveformCollapseBuilder::new()); // 现在将起点设置为随机起始区域 let (start_x, start_y) = random_start_position(rng); builder.with(AreaStartingPosition::new(start_x, start_y)); // 设置出口并生成怪物 builder.with(VoronoiSpawning::new()); builder.with(DistantExit::new()); } }
总结
门的教程就到这里了! 未来肯定还有改进的空间 - 但该功能已经可以工作了。 你可以靠近一扇门,它会阻挡移动和视线(因此房间的居住者不会打扰你)。 打开它,你就可以看穿 - 居住者也可以看到你。 现在它打开了,你可以通过它。 这非常接近门的定义了!
...
本章的源代码可以在这里找到
在您的浏览器中使用 web assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。