关于本教程
本教程是免费且开源的,所有代码均使用 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
,并查询它而不是整个列表) - 但我们实际上没有任何性能问题,并且地图构建在主循环之外运行(所以它是每个级别一次,而不是每帧) - 所以你很可能不需要它。
在我们的 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());
}
}
门的教程就到这里了! 未来肯定还有改进的空间 - 但该功能已经可以工作了。 你可以靠近一扇门,它会阻挡移动和视线(因此房间的居住者不会打扰你)。 打开它,你就可以看穿 - 居住者也可以看到你。 现在它打开了,你可以通过它。 这非常接近门的定义了!
...
本章的源代码可以在这里找到
版权所有 (C) 2019, Herbert Wolverson。