难度
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
目前,你可以通过多个地下城层级 - 但它们都具有相同的生成物 (spawn)。 随着你的前进,难度没有提升,也没有简单的模式可以帮助你度过初期。 本章旨在改变这一点。
添加等待按键
大多数 Roguelike 游戏的一个重要的战术要素是能够跳过回合 - 让怪物向你靠近(而不是让你先受到攻击!)。 作为将游戏转变为更具战术挑战性的一部分,让我们快速实现回合跳过。 在 player.rs
中(以及其余的输入),我们将数字键盘 5 和空格键添加为跳过:
#![allow(unused)] fn main() { // 跳过回合 VirtualKeyCode::Numpad5 => return RunState::PlayerTurn, VirtualKeyCode::Space => return RunState::PlayerTurn, }
这为游戏增加了一个不错的战术维度:你可以引诱敌人向你靠近,并从战术位置中获益。 Roguelike 游戏中另一个常见的功能是,如果没有附近的敌人,等待可以提供一些治疗。 我们只为玩家实现这一点,因为怪物突然治疗会让人感到不安! 因此,我们将其更改为:
#![allow(unused)] fn main() { // 跳过回合 VirtualKeyCode::Numpad5 => return skip_turn(&mut gs.ecs), VirtualKeyCode::Space => return skip_turn(&mut gs.ecs), }
现在我们实现 skip_turn
:
#![allow(unused)] fn main() { fn skip_turn(ecs: &mut World) -> RunState { let player_entity = ecs.fetch::<Entity>(); let viewshed_components = ecs.read_storage::<Viewshed>(); let monsters = ecs.read_storage::<Monster>(); let worldmap_resource = ecs.fetch::<Map>(); let mut can_heal = true; let viewshed = viewshed_components.get(*player_entity).unwrap(); for tile in viewshed.visible_tiles.iter() { let idx = worldmap_resource.xy_idx(tile.x, tile.y); for entity_id in worldmap_resource.tile_content[idx].iter() { let mob = monsters.get(*entity_id); match mob { None => {} Some(_) => { can_heal = false; } } } } if can_heal { let mut health_components = ecs.write_storage::<CombatStats>(); let player_hp = health_components.get_mut(*player_entity).unwrap(); player_hp.hp = i32::min(player_hp.hp + 1, player_hp.max_hp); } RunState::PlayerTurn } }
这段代码查找了各种实体,然后使用 tile_content
系统迭代玩家的视野范围 (viewshed)。 它检查玩家可以看到的怪物; 如果没有怪物出现,它会治疗玩家 1 点生命值。 这鼓励了动脑筋的玩法 - 并且可以在稍后通过包含饥饿时钟来平衡。 这也使游戏变得非常容易 - 但我们正在解决这个问题!
随着深入地下城难度增加:生成表 (spawn table)
到目前为止,我们一直在使用一个简单的生成系统:它随机选择一些怪物和物品,然后以相等的权重选择每个。 这不太像“正常”游戏,正常游戏倾向于使某些东西稀有 - 而某些东西常见。 我们将创建一个通用的 random_table
系统,用于生成系统。 创建一个新文件 random_table.rs
并将以下内容放入其中:
#![allow(unused)] fn main() { use rltk::RandomNumberGenerator; 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 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) -> RandomTable { self.total_weight += weight; self.entries.push(RandomEntry::new(name.to_string(), weight)); self } 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() } } }
因此,这创建了一个新类型 random_table
。 它为其添加了一个 new
方法,以方便创建一个新的 random_table
。 它还创建了一个 vector
或条目 (entry),每个条目都有一个权重和一个名称(传递字符串不是很有效,但可以使示例代码清晰!)。 它还实现了一个 add
函数,该函数允许你传入一个新的名称和权重,并更新结构的 total_weight
。 最后,roll
从 0 .. total_weight - 1
进行一次掷骰子 (dice roll),并遍历条目。 如果掷骰子的结果低于权重,则返回它 - 否则,它会从掷骰子的结果中减去权重并测试下一个条目。 这为表中的任何给定项目提供了等于条目相对权重的机会。 其中有一些额外的工作来帮助将方法链接在一起,以实现链式函数调用的 Rust 风格外观。 我们将在 spawner.rs
中使用它来创建一个新函数 room_table
:
#![allow(unused)] fn main() { fn room_table() -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1) .add("Health Potion", 7) .add("Fireball Scroll", 2) .add("Confusion Scroll", 2) .add("Magic Missile Scroll", 4) } }
这包含了我们到目前为止添加的所有物品和怪物,并附加了权重。 我对这些权重不是很在意; 我们稍后会调整它们! 这确实意味着调用 room_table().roll(rng)
将返回一个随机房间条目。
现在我们简化一下。 删除 spawner.rs
中的 NUM_MONSTERS
、random_monster
和 random_item
函数。 然后我们将房间生成代码替换为:
#![allow(unused)] fn main() { #[allow(clippy::map_entry)] pub fn spawn_room(ecs: &mut World, room : &Rect) { let spawn_table = room_table(); let mut spawn_points : HashMap<usize, String> = HashMap::new(); // Scope to keep the borrow checker happy { let mut rng = ecs.write_resource::<RandomNumberGenerator>(); let num_spawns = rng.roll_dice(1, MAX_MONSTERS + 3) - 3; for _i in 0 .. num_spawns { let mut added = false; let mut tries = 0; while !added && tries < 20 { let x = (room.x1 + rng.roll_dice(1, i32::abs(room.x2 - room.x1))) as usize; let y = (room.y1 + rng.roll_dice(1, i32::abs(room.y2 - room.y1))) as usize; let idx = (y * MAPWIDTH) + x; if !spawn_points.contains_key(&idx) { spawn_points.insert(idx, spawn_table.roll(&mut rng)); added = true; } else { tries += 1; } } } } // Actually spawn the monsters for spawn in spawn_points.iter() { let x = (*spawn.0 % MAPWIDTH) as i32; let y = (*spawn.0 / MAPWIDTH) as i32; 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), _ => {} } } } }
让我们逐步了解一下:
- 第一行告诉 Rust linter,我们确实喜欢检查
HashMap
的成员资格,然后插入其中 - 我们还设置了一个标志,但这与它的建议不太相符。 - 我们获取全局随机数生成器,并将生成数量设置为 1d7-3(范围为 -2 到 4)。
- 对于上面 0 的每个生成数量,我们选择房间中的一个随机点。 我们不断选择随机点,直到找到一个空点(或者我们超过 20 次尝试,在这种情况下我们放弃)。 找到一个点后,我们将其添加到
spawn
列表,其中包含位置和从我们的随机表中掷骰子的结果。 - 然后我们迭代生成列表,匹配掷骰子的结果并生成怪物和物品。
这绝对比以前的方法更简洁,现在你不太可能遇到兽人 - 而更有可能遇到地精和治疗药水。
快速的 cargo run
向你展示了改进的生成多样性。
随着深入地下城增加生成率
这给出了一个更好的分布,但没有解决后期关卡与早期关卡难度相同的问题。 一种快速而粗糙的方法是在你深入时生成更多实体。 这仍然不能解决问题,但这只是一个开始! 我们将首先修改 spawn_room
的函数签名以接受地图深度:
#![allow(unused)] fn main() { pub fn spawn_room(ecs: &mut World, room : &Rect, map_depth: i32) { }
然后我们将更改生成的实体数量以使用它:
#![allow(unused)] fn main() { let num_spawns = rng.roll_dice(1, MAX_MONSTERS + 3) + (map_depth - 1) - 3; }
我们将必须更改 main.rs
中的几个调用以传入深度:
#![allow(unused)] fn main() { for room in map.rooms.iter().skip(1) { spawner::spawn_room(&mut gs.ecs, room, 1); } }
#![allow(unused)] fn main() { // 构建新地图并放置玩家 let worldmap; let current_depth; { let mut worldmap_resource = self.ecs.write_resource::<Map>(); current_depth = worldmap_resource.depth; *worldmap_resource = Map::new_map_rooms_and_corridors(current_depth + 1); worldmap = worldmap_resource.clone(); } // 生成坏人 for room in worldmap.rooms.iter().skip(1) { spawner::spawn_room(&mut self.ecs, room, current_depth+1); } }
如果你现在 cargo run
,第一层会非常安静。 随着你的深入,难度会略有提升,直到你拥有真正的怪物大军!
按深度增加权重
让我们修改 room_table
函数以包含地图深度:
#![allow(unused)] fn main() { fn room_table(map_depth: i32) -> RandomTable { RandomTable::new() .add("Goblin", 10) .add("Orc", 1 + map_depth) .add("Health Potion", 7) .add("Fireball Scroll", 2 + map_depth) .add("Confusion Scroll", 2 + map_depth) .add("Magic Missile Scroll", 4) } }
我们还在 spawn_room
中更改了对它的调用以使用它:
#![allow(unused)] fn main() { let spawn_table = room_table(map_depth); }
在 cargo build
之后,瞧 - 你有了一个随着你的深入而增加找到兽人、火球和混乱卷轴的概率。 地精、治疗药水和魔法飞弹卷轴的总权重保持不变 - 但由于其他权重发生了变化,它们的总可能性降低了。
总结
你现在拥有了一个随着你的深入而难度增加的地下城! 在下一章中,我们还将研究如何通过装备让你的角色获得一些成长,以平衡局面。
本章的源代码可以在这里找到
在你的浏览器中使用 web assembly 运行本章的示例 (需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson.