回溯
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续创作,请考虑支持我的 Patreon。
设计文档中提到了使用 城镇传送门 (Town Portal) 返回城镇,这意味着 回溯 (backtracking) 是可能的——也就是说,可以返回到之前的关卡。 这在诸如 Dungeon Crawl: Stone Soup 之类的游戏中是很常见的功能(在这些游戏中,标准操作是将物品留在“储藏点 (stash)” 中,希望怪物不会找到它们)。
如果我们要支持在关卡之间来回移动(通过入口/出口对,或者通过传送/传送门等机制),我们需要调整我们处理关卡以及在关卡之间转换的方式。
主地牢地图 (A Master Dungeon Map)
我们将从创建一个结构开始,用于存储 所有 我们的地图——MasterDungeonMap
。创建一个新文件 map/dungeon.rs
,我们将开始将其组合在一起:
#![allow(unused)] fn main() { use std::collections::HashMap; use serde::{Serialize, Deserialize}; use super::{Map}; #[derive(Default, Serialize, Deserialize, Clone)] pub struct MasterDungeonMap { maps : HashMap<i32, Map> } impl MasterDungeonMap { pub fn new() -> MasterDungeonMap { MasterDungeonMap{ maps: HashMap::new() } } pub fn store_map(&mut self, map : &Map) { self.maps.insert(map.depth, map.clone()); } pub fn get_map(&self, depth : i32) -> Option<Map> { if self.maps.contains_key(&depth) { let mut result = self.maps[&depth].clone(); result.tile_content = vec![Vec::new(); (result.width * result.height) as usize]; Some(result) } else { None } } } }
这很容易理解:结构本身只有一个私有(没有 pub
)字段——maps
。它是一个 HashMap
——一个字典——由 Map
结构组成,并以地图深度为索引。我们提供了一个构造函数 new
以方便创建类,以及用于 store_map
(保存地图)和 get_map
(检索地图,返回 Option
,其中 None
表示我们没有该地图)的函数。我们还添加了 Serde
装饰器,使该结构可序列化——这样你就可以保存游戏了。我们还重新创建了 tile_content
字段,因为我们不序列化它。
在 map/mod.rs
中,你需要添加一行:pub mod dungeon;
。这告诉模块向外界公开 dungeon。
添加向后出口 (Adding backwards exits)
让我们向世界添加向上楼梯。在 map/tiletype.rs
中,我们添加新的类型:
#![allow(unused)] fn main() { #[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] pub enum TileType { Wall, Floor, DownStairs, Road, Grass, ShallowWater, DeepWater, WoodFloor, Bridge, Gravel, UpStairs } }
然后在 themes.rs 中,我们添加一些缺失的模式来渲染它(在每个主题中):
#![allow(unused)] fn main() { TileType::UpStairs => { glyph = rltk::to_cp437('<'); fg = RGB::from_f32(0., 1.0, 1.0); } }
在我们创建新地图时存储它们 (Storing New Maps As We Make Them)
目前,每当玩家进入新关卡时,我们都会在 main.rs
中调用 generate_world_map
来从头开始创建一个新地图。相反,我们希望将整个地牢地图作为全局资源——并在我们创建新地图时引用它,如果可能,使用 现有的 地图。将它放在 main.rs
中也很混乱,所以我们将借此机会将其重构到我们的地图系统中。
我们可以首先向 ECS World
添加一个 MasterDungeonMap
资源。在你的 main
函数中,在 ecs.insert
调用的顶部,添加一行以将 MasterDungeonMap
插入到 World
中(我包含了它后面的那行代码,以便你可以看到它放在哪里):
#![allow(unused)] fn main() { gs.ecs.insert(map::MasterDungeonMap::new()); gs.ecs.insert(Map::new(1, 64, 64, "New Map")); }
我们希望在每次开始新游戏时重置 MasterDungeonMap
,因此我们将相同的代码行添加到 game_over_cleanup
中:
#![allow(unused)] fn main() { fn game_over_cleanup(&mut self) { // 删除所有实体 (Delete everything) let mut to_delete = Vec::new(); for e in self.ecs.entities().join() { to_delete.push(e); } for del in to_delete.iter() { self.ecs.delete_entity(*del).expect("Deletion failed"); } // 生成一个新的玩家 (Spawn a new player) { let player_entity = spawner::player(&mut self.ecs, 0, 0); let mut player_entity_writer = self.ecs.write_resource::<Entity>(); *player_entity_writer = player_entity; } // 替换世界地图 (Replace the world maps) self.ecs.insert(map::MasterDungeonMap::new()); // 构建一个新的地图并放置玩家 (Build a new map and place the player) self.generate_world_map(1, 0); } }
现在我们将 generate_world_map
简化为基本功能:
#![allow(unused)] fn main() { fn generate_world_map(&mut self, new_depth : i32) { self.mapgen_index = 0; self.mapgen_timer = 0.0; self.mapgen_history.clear(); let map_building_info = map::level_transition(&mut self.ecs, new_depth); if let Some(history) = map_building_info { self.mapgen_history = history; } } }
此函数重置了构建器信息(这很好,因为它正在处理它自己的职责——但不是其他的),并询问一个新函数 map::level_transition
是否有历史信息。 如果有,它将其存储为地图构建历史; 否则,它将历史记录留空。
在 map/dungeon.rs
中,我们将构建它调用的外部函数(并记住将其添加到 map/mod.rs
中的 pub use
部分!):
#![allow(unused)] fn main() { pub fn level_transition(ecs : &mut World, new_depth: i32) -> Option<Vec<Map>> { // 获取主地牢地图 (Obtain the master dungeon map) let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); // 我们已经有地图了吗? (Do we already have a map?) if dungeon_master.get_map(new_depth).is_some() { std::mem::drop(dungeon_master); transition_to_existing_map(ecs, new_depth); None } else { std::mem::drop(dungeon_master); Some(transition_to_new_map(ecs, new_depth)) } } }
此函数从 ECS World 获取主地图,并调用 get_map
。 如果存在地图,则调用 transition_to_existing_map
。 如果不存在,则调用 transition_to_new_map
。 请注意 std::mem::drop
调用:从 World 获取 dungeon_master
会持有对它的“借用 (borrow) ”; 我们需要在将 ECS 传递给其他函数之前停止借用(drop 它),以避免多重引用问题。
新函数 transition_to_new_map
是来自旧 generate_world_map
函数的代码,经过修改后不再依赖于 self
。它在末尾有一个新部分:
#![allow(unused)] fn main() { fn transition_to_new_map(ecs : &mut World, new_depth: i32) -> Vec<Map> { let mut rng = ecs.write_resource::<rltk::RandomNumberGenerator>(); let mut builder = level_builder(new_depth, &mut rng, 80, 50); builder.build_map(&mut rng); if new_depth > 1 { if let Some(pos) = &builder.build_data.starting_position { let up_idx = builder.build_data.map.xy_idx(pos.x, pos.y); builder.build_data.map.tiles[up_idx] = TileType::UpStairs; } } let mapgen_history = builder.build_data.history.clone(); let player_start; { let mut worldmap_resource = ecs.write_resource::<Map>(); *worldmap_resource = builder.build_data.map.clone(); player_start = builder.build_data.starting_position.as_mut().unwrap().clone(); } // 生成坏人 (Spawn bad guys) std::mem::drop(rng); builder.spawn_entities(ecs); // 放置玩家并更新资源 (Place the player and update resources) let (player_x, player_y) = (player_start.x, player_start.y); let mut player_position = ecs.write_resource::<Point>(); *player_position = Point::new(player_x, player_y); let mut position_components = ecs.write_storage::<Position>(); let player_entity = ecs.fetch::<Entity>(); let player_pos_comp = position_components.get_mut(*player_entity); if let Some(player_pos_comp) = player_pos_comp { player_pos_comp.x = player_x; player_pos_comp.y = player_y; } // 标记玩家的视野为脏 (Mark the player's visibility as dirty) let mut viewshed_components = ecs.write_storage::<Viewshed>(); let vs = viewshed_components.get_mut(*player_entity); if let Some(vs) = vs { vs.dirty = true; } // 存储新生成的地图 (Store the newly minted map) let mut dungeon_master = ecs.write_resource::<MasterDungeonMap>(); dungeon_master.store_map(&builder.build_data.map); mapgen_history } }
最后,它返回构建历史记录。在此之前,它获取对新的 MasterDungeonMap
系统的访问权限,并将新地图添加到存储的地图列表中。我们还在起始位置添加了一个“向上”楼梯。
检索我们之前访问过的地图 (Retrieving maps we've visited before)
现在我们需要处理加载之前的地图!是时候充实 transition_to_existing_map
了:
#![allow(unused)] fn main() { fn transition_to_existing_map(ecs: &mut World, new_depth: i32) { let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); let map = dungeon_master.get_map(new_depth).unwrap(); let mut worldmap_resource = ecs.write_resource::<Map>(); let player_entity = ecs.fetch::<Entity>(); // 找到向下楼梯并放置玩家 (Find the down stairs and place the player) let w = map.width; for (idx, tt) in map.tiles.iter().enumerate() { if *tt == TileType::DownStairs { let mut player_position = ecs.write_resource::<Point>(); *player_position = Point::new(idx as i32 % w, idx as i32 / w); let mut position_components = ecs.write_storage::<Position>(); let player_pos_comp = position_components.get_mut(*player_entity); if let Some(player_pos_comp) = player_pos_comp { player_pos_comp.x = idx as i32 % w; player_pos_comp.y = idx as i32 / w; } } } *worldmap_resource = map; // 标记玩家的视野为脏 (Mark the player's visibility as dirty) let mut viewshed_components = ecs.write_storage::<Viewshed>(); let vs = viewshed_components.get_mut(*player_entity); if let Some(vs) = vs { vs.dirty = true; } } }
这非常简单:我们从地牢主地图列表中获取地图,并将其存储为 World
中的当前地图。我们扫描地图以查找向下楼梯,并将玩家放在上面。我们还标记玩家的视野为脏,以便在新地图中重新计算。
上一级关卡的输入 (Input for previous level)
现在我们需要处理实际的转换。由于我们使用 RunState::NextLevel
处理下楼梯,我们将添加一个状态来处理返回上一层:
#![allow(unused)] fn main() { #[derive(PartialEq, Copy, Clone)] pub enum RunState { AwaitingInput, PreRun, PlayerTurn, MonsterTurn, ShowInventory, ShowDropItem, ShowTargeting { range : i32, item : Entity}, MainMenu { menu_selection : gui::MainMenuSelection }, SaveGame, NextLevel, PreviousLevel, ShowRemoveItem, GameOver, MagicMapReveal { row : i32 }, MapGeneration } }
我们还需要在我们的状态匹配函数中处理它。我们基本上会复制“下一关 (next level)”选项:
#![allow(unused)] fn main() { RunState::PreviousLevel => { self.goto_previous_level(); self.mapgen_next_state = Some(RunState::PreRun); newrunstate = RunState::MapGeneration; } }
我们将复制/粘贴 goto_next_level()
和 goto_previous_level()
并更改一些数字:
#![allow(unused)] fn main() { fn goto_previous_level(&mut self) { // 删除不在玩家或其装备中的实体 (Delete entities that aren't the player or his/her equipment) let to_delete = self.entities_to_remove_on_level_change(); for target in to_delete { self.ecs.delete_entity(target).expect("Unable to delete entity"); } // 构建一个新的地图并放置玩家 (Build a new map and place the player) let current_depth; { let worldmap_resource = self.ecs.fetch::<Map>(); current_depth = worldmap_resource.depth; } self.generate_world_map(current_depth - 1); // 通知玩家并给他们一些生命值 (Notify the player and give them some health) let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>(); gamelog.entries.push("你回到了上一层关卡。 (You ascend to the previous level.)".to_string()); } }
接下来,在 player.rs
(我们处理输入的地方)——我们需要处理接收“向上走 (go up)”指令。 同样,我们基本上会复制“向下走 (go down)”:
#![allow(unused)] fn main() { VirtualKeyCode::Comma => { if try_previous_level(&mut gs.ecs) { return RunState::PreviousLevel; } } }
反过来,这要求我们复制 try_next_level
并创建 try_previous_level
:
#![allow(unused)] fn main() { pub fn try_previous_level(ecs: &mut World) -> bool { let player_pos = ecs.fetch::<Point>(); let map = ecs.fetch::<Map>(); let player_idx = map.xy_idx(player_pos.x, player_pos.y); if map.tiles[player_idx] == TileType::UpStairs { true } else { let mut gamelog = ecs.fetch_mut::<GameLog>(); gamelog.entries.push("这里没有上去的路。 (There is no way up from here.)".to_string()); false } } }
如果你现在 cargo run
,你就可以在地图之间转换了。但是,当你返回时——这里却是一座鬼城。关卡里 没有 其他任何人。 阴森恐怖,你妈妈的失踪应该让你沮丧!
实体冻结和解冻 (Entity freezing and unfreezing)
如果你回想一下本教程的第一部分,我们花了一些时间确保在更改关卡时 删除 除了玩家之外的所有内容。 这很有道理:你永远不会回来了,所以为什么要浪费内存来保存它们呢? 现在我们能够来回移动了,我们需要跟踪事物的所在位置——这样我们才能再次找到它们。 我们还可以借此机会稍微清理一下我们的转换——所有这些函数都很混乱!
考虑一下我们想要做什么,我们的目标是存储实体 在另一个关卡 上的位置。 因此我们需要存储关卡,以及他们的 x/y
位置。 让我们创建一个新的组件。 在 components.rs
中(并在 main.rs
和 saveload_system.rs
中注册):
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct OtherLevelPosition { pub x: i32, pub y: i32, pub depth: i32 } }
实际上,我们可以创建一个相对简单的函数来调整我们的实体状态。 在 map/dungeon.rs
中,我们将创建一个新函数:
#![allow(unused)] fn main() { pub fn freeze_level_entities(ecs: &mut World) { // 获取 ECS 访问权限 (Obtain ECS access) let entities = ecs.entities(); let mut positions = ecs.write_storage::<Position>(); let mut other_level_positions = ecs.write_storage::<OtherLevelPosition>(); let player_entity = ecs.fetch::<Entity>(); let map_depth = ecs.fetch::<Map>().depth; // 查找位置并创建 OtherLevelPosition (Find positions and make OtherLevelPosition) let mut pos_to_delete : Vec<Entity> = Vec::new(); for (entity, pos) in (&entities, &positions).join() { if entity != *player_entity { other_level_positions.insert(entity, OtherLevelPosition{ x: pos.x, y: pos.y, depth: map_depth }).expect("Insert fail"); pos_to_delete.push(entity); } } // 移除位置 (Remove positions) for p in pos_to_delete.iter() { positions.remove(*p); } } }
这是另一个相对简单的函数:我们获取对各种存储的访问权限,然后迭代所有具有位置的实体。 我们检查它是否不是玩家(因为他们的处理方式不同); 如果他们 不是 ——我们为他们添加一个 OtherLevelPosition
,并在 pos_to_delete
向量中标记它们。 然后我们迭代该向量,并从我们标记的每个人中删除 Position
组件。
让他们恢复生机(解冻)也很容易:
#![allow(unused)] fn main() { pub fn thaw_level_entities(ecs: &mut World) { // 获取 ECS 访问权限 (Obtain ECS access) let entities = ecs.entities(); let mut positions = ecs.write_storage::<Position>(); let mut other_level_positions = ecs.write_storage::<OtherLevelPosition>(); let player_entity = ecs.fetch::<Entity>(); let map_depth = ecs.fetch::<Map>().depth; // 查找 OtherLevelPosition (Find OtherLevelPosition) let mut pos_to_delete : Vec<Entity> = Vec::new(); for (entity, pos) in (&entities, &other_level_positions).join() { if entity != *player_entity && pos.depth == map_depth { positions.insert(entity, Position{ x: pos.x, y: pos.y }).expect("Insert fail"); pos_to_delete.push(entity); } } // 移除位置 (Remove positions) for p in pos_to_delete.iter() { other_level_positions.remove(*p); } } }
这基本上是相同的函数,但逻辑相反! 我们 添加 Position
组件,并 删除 OtherLevelPosition
组件。
在 main.rs
中,我们有一堆 goto_next_level
和 goto_previous_level
函数的混乱代码。 让我们用一个通用的函数替换它们,该函数可以理解我们要往哪个方向走:
#![allow(unused)] fn main() { fn goto_level(&mut self, offset: i32) { freeze_level_entities(&mut self.ecs); // 构建一个新的地图并放置玩家 (Build a new map and place the player) let current_depth = self.ecs.fetch::<Map>().depth; self.generate_world_map(current_depth + offset, offset); // 通知玩家 (Notify the player) let mut gamelog = self.ecs.fetch_mut::<gamelog::GameLog>(); gamelog.entries.push("你改变了关卡。 (You change level.)".to_string()); } }
这要简单得多——我们调用新的 freeze_level_entities
函数,获取当前深度,并使用新深度调用 generate_world_map
。 这是什么? 我们还在传递 offset
。我们需要知道你往哪个方向走,否则你可以通过先返回再前进的方式完成整个关卡——并被传送到“向下 (down)”的楼梯! 因此,我们将修改 generate_world_map
以接受此参数:
#![allow(unused)] fn main() { fn generate_world_map(&mut self, new_depth : i32, offset: i32) { self.mapgen_index = 0; self.mapgen_timer = 0.0; self.mapgen_history.clear(); let map_building_info = map::level_transition(&mut self.ecs, new_depth, offset); if let Some(history) = map_building_info { self.mapgen_history = history; } else { map::thaw_level_entities(&mut self.ecs); } } }
请注意,我们基本上是在调用相同的代码,但也向 level_transition
传递了 offset
(稍后会详细介绍)。 如果我们没有创建新地图,我们还会调用 thaw
。 这样,新地图会获得新实体——旧地图会获得旧实体。
你需要修复对 generate_world_map
的各种调用。 如果你正在创建新关卡,则可以传递 0
作为偏移量。 你还需要修复更改关卡的两个 match
条目:
#![allow(unused)] fn main() { RunState::NextLevel => { self.goto_level(1); self.mapgen_next_state = Some(RunState::PreRun); newrunstate = RunState::MapGeneration; } RunState::PreviousLevel => { self.goto_level(-1); self.mapgen_next_state = Some(RunState::PreRun); newrunstate = RunState::MapGeneration; } }
最后,我们需要打开 dungeon.rs
并对关卡转换系统进行简单的更改,以处理偏移量:
#![allow(unused)] fn main() { pub fn level_transition(ecs : &mut World, new_depth: i32, offset: i32) -> Option<Vec<Map>> { // 获取主地牢地图 (Obtain the master dungeon map) let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); // 我们已经有地图了吗? (Do we already have a map?) if dungeon_master.get_map(new_depth).is_some() { std::mem::drop(dungeon_master); transition_to_existing_map(ecs, new_depth, offset); None } else { std::mem::drop(dungeon_master); Some(transition_to_new_map(ecs, new_depth)) } } }
这里唯一的区别是我们将偏移量传递给 transition_to_existing_map
。 这是更新后的函数:
#![allow(unused)] fn main() { fn transition_to_existing_map(ecs: &mut World, new_depth: i32, offset: i32) { let dungeon_master = ecs.read_resource::<MasterDungeonMap>(); let map = dungeon_master.get_map(new_depth).unwrap(); let mut worldmap_resource = ecs.write_resource::<Map>(); let player_entity = ecs.fetch::<Entity>(); // 找到向下楼梯并放置玩家 (Find the down stairs and place the player) let w = map.width; let stair_type = if offset < 0 { TileType::DownStairs } else { TileType::UpStairs }; for (idx, tt) in map.tiles.iter().enumerate() { if *tt == stair_type { ... }
我们更新了签名,并使用它来确定玩家的放置位置。 如果偏移量小于 0,我们想要一个向下楼梯——否则我们想要一个向上楼梯。
你现在可以 cargo run
了,并在关卡之间来回跳转,尽情享受——每个关卡上的实体都将停留在你离开它们的地方!
保存/加载游戏 (Saving/Loading the game)
现在我们需要将地牢主地图包含在我们的保存游戏中; 否则,重新加载将保留当前地图并生成一大堆新地图——实体放置无效! 我们需要扩展我们的序列化系统以保存整个地牢地图,而不仅仅是当前的地图。
我们将从 components.rs
开始; 你可能还记得,我们必须创建一个特殊的 SerializationHelper
来帮助我们将地图保存为游戏的一部分。 它看起来像这样:
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct SerializationHelper { pub map : super::map::Map } }
我们需要 第二个,来存储 MasterDungeonMap
。 它看起来像这样:
#![allow(unused)] fn main() { #[derive(Component, Serialize, Deserialize, Clone)] pub struct DMSerializationHelper { pub map : super::map::MasterDungeonMap } }
在 main.rs
中,我们必须像其他组件一样注册它:
#![allow(unused)] fn main() { gs.ecs.register::<DMSerializationHelper>(); }
在 saveload.rs
中,我们需要将其包含在组件类型的大列表中。 同样在 saveload.rs
中,我们需要使用之前使用过的相同技巧将其添加到 ECS World,保存它,然后删除它:
#![allow(unused)] fn main() { pub fn save_game(ecs : &mut World) { // 创建 helper (Create helper) let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone(); let dungeon_master = ecs.get_mut::<super::map::MasterDungeonMap>().unwrap().clone(); let savehelper = ecs .create_entity() .with(SerializationHelper{ map : mapcopy }) .marked::<SimpleMarker<SerializeMe>>() .build(); let savehelper2 = ecs .create_entity() .with(DMSerializationHelper{ map : dungeon_master }) .marked::<SimpleMarker<SerializeMe>>() .build(); // 实际序列化 (Actually serialize) { let data = ( ecs.entities(), ecs.read_storage::<SimpleMarker<SerializeMe>>() ); let writer = File::create("./savegame.json").unwrap(); let mut serializer = serde_json::Serializer::new(writer); serialize_individually!(ecs, serializer, data, Position, Renderable, Player, Viewshed, Monster, Name, BlocksTile, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleeWeapon, Wearable, WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden, EntryTrigger, EntityMoved, SingleActivation, BlocksVisibility, Door, Bystander, Vendor, Quips, Attributes, Skills, Pools, NaturalAttackDefense, LootTable, Carnivore, Herbivore, OtherLevelPosition, DMSerializationHelper ); } // 清理 (Clean up) ecs.delete_entity(savehelper).expect("Crash on cleanup"); ecs.delete_entity(savehelper2).expect("Crash on cleanup"); } }
请注意,我们正在创建 第二个 临时实体——savehelper2
。 这确保了数据与所有其他数据一起保存。 我们在最后一行将其删除。 我们还需要调整我们的加载器:
#![allow(unused)] fn main() { pub fn load_game(ecs: &mut World) { { // 删除所有实体 (Delete everything) let mut to_delete = Vec::new(); for e in ecs.entities().join() { to_delete.push(e); } for del in to_delete.iter() { ecs.delete_entity(*del).expect("Deletion failed"); } } let data = fs::read_to_string("./savegame.json").unwrap(); let mut de = serde_json::Deserializer::from_str(&data); { let mut d = (&mut ecs.entities(), &mut ecs.write_storage::<SimpleMarker<SerializeMe>>(), &mut ecs.write_resource::<SimpleMarkerAllocator<SerializeMe>>()); deserialize_individually!(ecs, de, d, Position, Renderable, Player, Viewshed, Monster, Name, BlocksTile, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem, WantsToDropItem, SerializationHelper, Equippable, Equipped, MeleeWeapon, Wearable, WantsToRemoveItem, ParticleLifetime, HungerClock, ProvidesFood, MagicMapper, Hidden, EntryTrigger, EntityMoved, SingleActivation, BlocksVisibility, Door, Bystander, Vendor, Quips, Attributes, Skills, Pools, NaturalAttackDefense, LootTable, Carnivore, Herbivore, OtherLevelPosition, DMSerializationHelper ); } let mut deleteme : Option<Entity> = None; let mut deleteme2 : Option<Entity> = None; { let entities = ecs.entities(); let helper = ecs.read_storage::<SerializationHelper>(); let helper2 = ecs.read_storage::<DMSerializationHelper>(); let player = ecs.read_storage::<Player>(); let position = ecs.read_storage::<Position>(); for (e,h) in (&entities, &helper).join() { let mut worldmap = ecs.write_resource::<super::map::Map>(); *worldmap = h.map.clone(); worldmap.tile_content = vec![Vec::new(); (worldmap.height * worldmap.width) as usize]; deleteme = Some(e); } for (e,h) in (&entities, &helper2).join() { let mut dungeonmaster = ecs.write_resource::<super::map::MasterDungeonMap>(); *dungeonmaster = h.map.clone(); deleteme2 = Some(e); } for (e,_p,pos) in (&entities, &player, &position).join() { let mut ppos = ecs.write_resource::<rltk::Point>(); *ppos = rltk::Point::new(pos.x, pos.y); let mut player_resource = ecs.write_resource::<Entity>(); *player_resource = e; } } ecs.delete_entity(deleteme.unwrap()).expect("Unable to delete helper"); ecs.delete_entity(deleteme2.unwrap()).expect("Unable to delete helper"); } }
因此,在这个代码中,我们添加了遍历 MasterDungeonMap
helper 的代码,并将其作为资源添加到 World
中——然后删除实体。 这与我们对 Map
所做的操作相同——但适用于 MasterDungeonMap
。
如果你现在 cargo run
,你可以转换关卡,保存游戏,然后再进行转换。 序列化工作正常!
更无缝的转换 (More seamless transition)
要求玩家输入仅使用一次的按键(用于向上/向下楼梯)来与楼梯交互不是很符合人体工程学。 不仅如此,使用国际键盘有时很难捕捉到正确的键码! 如果走进楼梯就可以带你到达楼梯的目的地,那肯定会更流畅。 同时,我们可以修复一些困扰我一段时间的问题:尝试移动失败会浪费一个回合,而你却在盲目地撞墙!
由于 player.rs
是我们处理输入的地方,让我们打开它。 我们将更改 try_move_player
以返回一个 RunState
:
#![allow(unused)] fn main() { pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::<Position>(); let players = ecs.read_storage::<Player>(); let mut viewsheds = ecs.write_storage::<Viewshed>(); let entities = ecs.entities(); let combat_stats = ecs.read_storage::<Attributes>(); let map = ecs.fetch::<Map>(); let mut wants_to_melee = ecs.write_storage::<WantsToMelee>(); let mut entity_moved = ecs.write_storage::<EntityMoved>(); 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>(); let bystanders = ecs.read_storage::<Bystander>(); let vendors = ecs.read_storage::<Vendor>(); let mut result = RunState::AwaitingInput; let mut swap_entities : Vec<(Entity, i32, i32)> = Vec::new(); 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 bystander = bystanders.get(*potential_target); let vendor = vendors.get(*potential_target); if bystander.is_some() || vendor.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; result = RunState::PlayerTurn; } 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 RunState::PlayerTurn; } } 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; } } if !map.blocked[destination_idx] { 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; result = RunState::PlayerTurn; } } 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; } } result } }
这与之前的函数基本相同,但我们确保从中返回一个 RunState
。 如果玩家确实移动了,我们返回 RunState::PlayerTurn
。 如果移动无效,我们返回 RunState::AwaitingInput
——表示我们仍在等待有效的指令。
在玩家键盘处理程序中,我们需要将每次调用 try_move_player...
替换为 return try_move_player...
:
#![allow(unused)] fn main() { ... match ctx.key { None => { return RunState::AwaitingInput } // 没有发生任何事情 (Nothing happened) Some(key) => match key { VirtualKeyCode::Left | VirtualKeyCode::Numpad4 | VirtualKeyCode::H => return try_move_player(-1, 0, &mut gs.ecs), VirtualKeyCode::Right | VirtualKeyCode::Numpad6 | VirtualKeyCode::L => return try_move_player(1, 0, &mut gs.ecs), VirtualKeyCode::Up | VirtualKeyCode::Numpad8 | VirtualKeyCode::K => return try_move_player(0, -1, &mut gs.ecs), VirtualKeyCode::Down | VirtualKeyCode::Numpad2 | VirtualKeyCode::J => return try_move_player(0, 1, &mut gs.ecs), // 对角线 (Diagonals) VirtualKeyCode::Numpad9 | VirtualKeyCode::U => return try_move_player(1, -1, &mut gs.ecs), VirtualKeyCode::Numpad7 | VirtualKeyCode::Y => return try_move_player(-1, -1, &mut gs.ecs), VirtualKeyCode::Numpad3 | VirtualKeyCode::N => return try_move_player(1, 1, &mut gs.ecs), VirtualKeyCode::Numpad1 | VirtualKeyCode::B => return try_move_player(-1, 1, &mut gs.ecs), // 跳过回合 (Skip Turn) VirtualKeyCode::Numpad5 | VirtualKeyCode::Space => return skip_turn(&mut gs.ecs), ... }
如果你现在 cargo run
,你会注意到你不再浪费回合撞墙了。
既然我们已经完成了这项工作,我们就可以很好地修改 try_move_player
,使其能够在玩家进入楼梯时返回关卡转换指令。 让我们在移动后添加一个楼梯检查,并在适用时返回楼梯转换:
#![allow(unused)] fn main() { if !map.blocked[destination_idx] { 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; result = RunState::PlayerTurn; match map.tiles[destination_idx] { TileType::DownStairs => result = RunState::NextLevel, TileType::UpStairs => result = RunState::PreviousLevel, _ => {} } } }
现在你可以通过跑到出口来更改关卡了。
关于楼梯舞步 (A Word on Stair dancing)
许多 Roguelike 游戏中都会遇到一个问题,那就是“楼梯舞步 (stair dancing)”。 你看到一个可怕的怪物,然后你退到楼梯上。 治疗一下,下来打怪物一下。 再跳回楼上,再治疗一下。 由于怪物被“冻结”在后面的关卡中,它不会追你上楼梯(除非在处理此问题的游戏中,例如 Dungeon Crawl Stone Soup!)。 这对于整体游戏来说可能是不希望看到的,但我们现在还不打算修复它。 计划在未来的章节中使 NPC AI 在总体上更加智能(并引入更多战术选项),因此我们将把这个问题留到以后解决。
总结 (Wrap Up)
这是另一个大型章节,但我们实现了一些非常有用的功能:关卡是持久的,你可以穿越世界,享受着当你返回时,你留在树林中的剑仍然会在那里的知识。 这在使游戏更可信、更广阔方面大有帮助(并且它开始感觉更“开放世界”,即使它不是!)。 我们将在以后的章节中添加城镇传送门,届时城镇将成为一个更有用的访问地点。
接下来——为了确保你不会感到无聊!——我们将添加下一个关卡,石灰岩洞穴。
...
本章的源代码可以在 这里 找到
在你的浏览器中使用 Web Assembly 运行本章的示例(需要 WebGL2)
版权所有 (C) 2019, Herbert Wolverson。