回溯


关于本教程

本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用。 我希望您会喜欢本教程,并制作出色的游戏!

如果您喜欢这个教程并希望我继续创作,请考虑支持我的 Patreon

实践 Rust


设计文档中提到了使用 城镇传送门 (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.rssaveload_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_levelgoto_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,
        _ => {}
    }
}
}

现在你可以通过跑到出口来更改关卡了。

Screenshot

关于楼梯舞步 (A Word on Stair dancing)

许多 Roguelike 游戏中都会遇到一个问题,那就是“楼梯舞步 (stair dancing)”。 你看到一个可怕的怪物,然后你退到楼梯上。 治疗一下,下来打怪物一下。 再跳回楼上,再治疗一下。 由于怪物被“冻结”在后面的关卡中,它不会追你上楼梯(除非在处理此问题的游戏中,例如 Dungeon Crawl Stone Soup!)。 这对于整体游戏来说可能是不希望看到的,但我们现在还不打算修复它。 计划在未来的章节中使 NPC AI 在总体上更加智能(并引入更多战术选项),因此我们将把这个问题留到以后解决。

总结 (Wrap Up)

这是另一个大型章节,但我们实现了一些非常有用的功能:关卡是持久的,你可以穿越世界,享受着当你返回时,你留在树林中的剑仍然会在那里的知识。 这在使游戏更可信、更广阔方面大有帮助(并且它开始感觉更“开放世界”,即使它不是!)。 我们将在以后的章节中添加城镇传送门,届时城镇将成为一个更有用的访问地点。

接下来——为了确保你不会感到无聊!——我们将添加下一个关卡,石灰岩洞穴。

...

本章的源代码可以在 这里 找到

在你的浏览器中使用 Web Assembly 运行本章的示例(需要 WebGL2)

版权所有 (C) 2019, Herbert Wolverson。