加载和保存游戏


关于本教程

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

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

Hands-On Rust


在前几个章节中,我们专注于让游戏变得可玩(即使不是非常有趣)。 你可以四处奔跑,杀死怪物,并使用各种物品。 这是一个好的开始! 大多数游戏都允许你停止游玩,并在稍后返回以继续。 幸运的是,Rust(和相关的库)使这变得相对容易。

主菜单

如果你要恢复游戏,你需要一个可以进行此操作的地方! 主菜单还让你有选择放弃上次保存、可能查看制作人员名单,并大致告诉世界你的游戏就在这里 - 并且是由你编写的。 拥有一个主菜单是很重要的,所以我们来创建一个。

处于菜单中是一种状态 - 因此我们将其添加到不断扩展的 RunState 枚举中。 我们希望将菜单状态包含在其中,因此定义最终看起来像这样:

#![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 }
}
}

gui.rs 中,我们添加几个枚举类型来处理主菜单选项:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuSelection { NewGame, LoadGame, Quit }

#[derive(PartialEq, Copy, Clone)]
pub enum MainMenuResult { NoSelection{ selected : MainMenuSelection }, Selected{ selected: MainMenuSelection } }
}

你的 GUI 现在可能在告诉你 main.rs 有错误! 它是对的 - 我们需要处理新的 RunState 选项。 我们需要稍微调整一下,以确保在菜单中时我们也不会渲染 GUI 和地图。 所以我们重新安排 tick

#![allow(unused)]
fn main() {
fn tick(&mut self, ctx : &mut Rltk) {
    let mut newrunstate;
    {
        let runstate = self.ecs.fetch::<RunState>();
        newrunstate = *runstate;
    }

    ctx.cls();

    match newrunstate {
        RunState::MainMenu{..} => {}
        _ => {
            draw_map(&self.ecs, ctx);

            {
                let positions = self.ecs.read_storage::<Position>();
                let renderables = self.ecs.read_storage::<Renderable>();
                let map = self.ecs.fetch::<Map>();

                let mut data = (&positions, &renderables).join().collect::<Vec<_>>();
                data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order) );
                for (pos, render) in data.iter() {
                    let idx = map.xy_idx(pos.x, pos.y);
                    if map.visible_tiles[idx] { ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) }
                }

                gui::draw_ui(&self.ecs, ctx);
            }
        }
    }
    ...
}

我们还将在 RunState 的大型 match 语句中处理 MainMenu 状态:

#![allow(unused)]
fn main() {
RunState::MainMenu{ .. } => {
    let result = gui::main_menu(self, ctx);
    match result {
        gui::MainMenuResult::NoSelection{ selected } => newrunstate = RunState::MainMenu{ menu_selection: selected },
        gui::MainMenuResult::Selected{ selected } => {
            match selected {
                gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun,
                gui::MainMenuSelection::LoadGame => newrunstate = RunState::PreRun,
                gui::MainMenuSelection::Quit => { ::std::process::exit(0); }
            }
        }
    }
}
}

我们基本上是在用新的菜单选择更新状态,如果选择了某些内容,我们会更改游戏状态。 对于 Quit,我们只是终止进程。 目前,我们将加载/开始游戏设置为执行相同的操作:进入 PreRun 状态以设置游戏。

最后要做的是编写菜单本身。 在 menu.rs 中:

pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
    let runstate = gs.ecs.fetch::<RunState>();

    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");

    if let RunState::MainMenu{ menu_selection : selection } = *runstate {
        if selection == MainMenuSelection::NewGame {
            ctx.print_color_centered(24, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Begin New Game");
        } else {
            ctx.print_color_centered(24, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Begin New Game");
        }

        if selection == MainMenuSelection::LoadGame {
            ctx.print_color_centered(25, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Load Game");
        } else {
            ctx.print_color_centered(25, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Load Game");
        }

        if selection == MainMenuSelection::Quit {
            ctx.print_color_centered(26, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Quit");
        } else {
            ctx.print_color_centered(26, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Quit");
        }

        match ctx.key {
            None => return MainMenuResult::NoSelection{ selected: selection },
            Some(key) => {
                match key {
                    VirtualKeyCode::Escape => { return MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
                    VirtualKeyCode::Up => {
                        let newselection;
                        match selection {
                            MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit,
                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame,
                            MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame
                        }
                        return MainMenuResult::NoSelection{ selected: newselection }
                    }
                    VirtualKeyCode::Down => {
                        let newselection;
                        match selection {
                            MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame,
                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit,
                            MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame
                        }
                        return MainMenuResult::NoSelection{ selected: newselection }
                    }
                    VirtualKeyCode::Return => return MainMenuResult::Selected{ selected : selection },
                    _ => return MainMenuResult::NoSelection{ selected: selection }
                }
            }
        }
    }

    MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }
}

这有点拗口,但它显示了菜单选项,并允许你使用向上/向下键和回车键来选择它们。 它非常小心地不修改状态本身,以保持清晰。

包含 Serde

Serde 几乎是 Rust 中序列化的黄金标准。 它使很多事情变得更容易! 所以第一步是包含它。 在你的项目的 Cargo.toml 文件中,我们将扩展 dependencies 部分以包含它:

[dependencies]
rltk = { version = "0.8.0", features = ["serde"] }
specs = { version = "0.16.1", features = ["serde"] }
specs-derive = "0.4.1"
serde= { version = "1.0.93", features = ["derive"] }
serde_json = "1.0.39"

现在可能值得调用 cargo run - 这将需要一段时间,下载新的依赖项(及其所有依赖项)并为你构建它们。 它应该将它们保留在本地,这样你就不必每次构建都等待这么久。

添加 "SaveGame" 状态

我们将再次扩展 RunState 以支持游戏保存:

#![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
}
}

tick 中,我们现在添加虚拟代码:

#![allow(unused)]
fn main() {
RunState::SaveGame => {
    newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
}
}

player.rs 中,我们将添加另一个键盘处理程序 - escape 键:

#![allow(unused)]
fn main() {
// 保存并退出
VirtualKeyCode::Escape => return RunState::SaveGame,
}

如果你现在 cargo run,你可以开始一个游戏并按下 escape 键退出到菜单。

开始保存游戏

现在脚手架已经到位,是时候实际保存一些东西了! 让我们从简单的开始,感受一下 Serde。 在 tick 函数中,我们扩展保存系统,将地图的 JSON 表示形式转储到控制台:

#![allow(unused)]
fn main() {
RunState::SaveGame => {
    let data = serde_json::to_string(&*self.ecs.fetch::<Map>()).unwrap();
    println!("{}", data);

    newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
}
}

我们还需要在 main.rs 的顶部添加 extern crate serde;

这将无法编译,因为我们需要告诉 Map 序列化自身! 幸运的是,serde 提供了一些助手来简化此操作。 在 map.rs 的顶部,我们添加 use serde::{Serialize, Deserialize};。 然后我们装饰地图以派生序列化和反序列化代码:

#![allow(unused)]
fn main() {
#[derive(Default, Serialize, Deserialize, Clone)]
pub struct Map {
    pub tiles : Vec<TileType>,
    pub rooms : Vec<Rect>,
    pub width : i32,
    pub height : i32,
    pub revealed_tiles : Vec<bool>,
    pub visible_tiles : Vec<bool>,
    pub blocked : Vec<bool>,

    #[serde(skip_serializing)]
    #[serde(skip_deserializing)]
    pub tile_content : Vec<Vec<Entity>>
}
}

请注意,我们已使用指令装饰了 tile_content,使其不进行序列化/反序列化。 这可以防止我们需要存储实体,并且由于此数据在每一帧都会重建 - 因此无关紧要。 游戏仍然无法编译; 我们需要向 TileTypeRect 添加类似的装饰器:

#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum TileType {
    Wall, Floor
}
}
#![allow(unused)]
fn main() {
#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)]
pub struct Rect {
    pub x1 : i32,
    pub x2 : i32,
    pub y1 : i32,
    pub y2 : i32
}
}

如果你现在 cargo run 项目,当你按下 escape 键时,它会将大量的 JSON 数据转储到控制台。 那就是游戏地图!

保存实体状态

现在我们已经了解了 serde 的用处,我们应该开始将其用于游戏本身。 这比人们预期的要困难,因为 specs 处理 Entity 结构的方式:它们的 ID 编号纯粹是合成的,无法保证你下次会得到相同的编号! 此外,你可能不想保存所有内容 - 因此 specs 引入了标记的概念来帮助解决这个问题。 最终结果比实际需要的要复杂一点,但它提供了一个非常强大的序列化系统。

引入标记 (Markers)

首先,在 main.rs 中,我们将告诉 Rust 我们想要使用标记功能:

#![allow(unused)]
fn main() {
use specs::saveload::{SimpleMarker, SimpleMarkerAllocator};
}

components.rs 中,我们将添加一个标记类型:

#![allow(unused)]
fn main() {
pub struct SerializeMe;
}

回到 main.rs 中,我们将 SerializeMe 添加到我们注册的事物列表中:

#![allow(unused)]
fn main() {
gs.ecs.register::<SimpleMarker<SerializeMe>>();
}

我们还将条目添加到 ECS 资源中,该条目用于确定下一个身份:

#![allow(unused)]
fn main() {
gs.ecs.insert(SimpleMarkerAllocator::<SerializeMe>::new());
}

最后,在 spawners.rs 中,我们告诉每个实体构建器包含标记。 这是 Player 的完整条目:

#![allow(unused)]
fn main() {
pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
    ecs
        .create_entity()
        .with(Position { x: player_x, y: player_y })
        .with(Renderable {
            glyph: rltk::to_cp437('@'),
            fg: RGB::named(rltk::YELLOW),
            bg: RGB::named(rltk::BLACK),
            render_order: 0
        })
        .with(Player{})
        .with(Viewshed{ visible_tiles : Vec::new(), range: 8, dirty: true })
        .with(Name{name: "Player".to_string() })
        .with(CombatStats{ max_hp: 30, hp: 30, defense: 2, power: 5 })
        .marked::<SimpleMarker<SerializeMe>>()
        .build()
}
}

新的一行 (.marked::<SimpleMarker<SerializeMe>>()) 需要在所有 spawners 文件中重复。 值得查看本章的源代码; 为了避免制作一个充满源代码的庞大章节,我省略了重复的细节。

ConvertSaveload 派生宏

Entity 类本身(由 Specs 提供)不能直接序列化; 它实际上是对一个名为 "slot map" 的特殊结构中的身份的引用(基本上是一种非常有效的方式来存储数据并保持位置稳定,直到你删除它,但在可用时重新使用空间)。 因此,为了保存和加载 Entity 类,有必要将这些合成身份转换为唯一的 ID 编号。 幸运的是,Specs 提供了一个名为 ConvertSaveloadderive 宏来实现此目的。 它适用于大多数组件,但并非所有组件!

序列化不包含 Entity 但确实包含数据的类型非常容易:使用 #[derive(Component, ConvertSaveload, Clone)] 标记它。 因此,我们遍历 components.rs 中的所有简单组件类型; 例如,这是 Position

#![allow(unused)]
fn main() {
#[derive(Component, ConvertSaveload, Clone)]
pub struct Position {
    pub x: i32,
    pub y: i32,
}
}

所以这表示:

  • 该结构是 Component。 如果你愿意,你可以用编写代码指定 Specs 存储来替换它,但是宏要容易得多!
  • ConvertSaveload 实际上添加了 SerializeDeserialize,但是为它遇到的任何 Entity 类添加了额外的转换。
  • Clone 表示“此结构可以从内存中的一个点复制到另一个点。” 这对于 Serde 的内部工作是必需的,并且还允许你将 .clone() 附加到对组件的任何引用的末尾 - 并获得另一个完美的副本。 在大多数情况下,clone 非常快(有时编译器可以使其完全不执行任何操作!)

当你有一个没有数据的组件时,ConvertSaveload 宏不起作用! 幸运的是,这些不需要任何额外的转换 - 因此你可以回退到默认的 Serde 语法。 这是一个非数据(“标签”)类:

#![allow(unused)]
fn main() {
#[derive(Component, Serialize, Deserialize, Clone)]
pub struct Player {}
}

实际保存一些东西

加载和保存的代码变得很大,因此我们已将其移至 saveload_system.rs。 然后在 main.rs 中包含 mod saveload_system;,并将 SaveGame 状态替换为:

#![allow(unused)]
fn main() {
RunState::SaveGame => {
    saveload_system::save_game(&mut self.ecs);
    newrunstate = RunState::MainMenu{ menu_selection : gui::MainMenuSelection::LoadGame };
}
}

那么... 开始实现 save_game。 Serde 和 Specs 可以很好地协同工作,但是桥梁仍然定义得相当粗糙。 我一直遇到诸如如果我有超过 16 种组件类型,它就无法编译的问题! 为了解决这个问题,我构建了一个。 我建议在您准备好学习 Rust(令人印象深刻)的宏系统之前,只需复制该宏即可。

#![allow(unused)]
fn main() {
macro_rules! serialize_individually {
    ($ecs:expr, $ser:expr, $data:expr, $( $type:ty),*) => {
        $(
        SerializeComponents::<NoError, SimpleMarker<SerializeMe>>::serialize(
            &( $ecs.read_storage::<$type>(), ),
            &$data.0,
            &$data.1,
            &mut $ser,
        )
        .unwrap();
        )*
    };
}
}

它的简短版本是,它将你的 ECS 作为第一个参数,并将一个元组与你的实体存储和“标记”存储在其中(你稍后会看到)。 之后的每个参数都是一个 类型 - 列出存储在你的 ECS 中的类型。 这些是重复规则,因此它为每种类型发出一个 SerializeComponent::serialize 调用。 它不如一次完成所有操作有效,但它可以工作 - 并且当你超过 16 种类型时不会崩溃! 然后 save_game 函数看起来像这样:

#![allow(unused)]
fn main() {
pub fn save_game(ecs : &mut World) {
    // 创建助手
    let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
    let savehelper = ecs
        .create_entity()
        .with(SerializationHelper{ map : mapcopy })
        .marked::<SimpleMarker<SerializeMe>>()
        .build();

    // 实际序列化
    {
        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, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
            AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
            WantsToDropItem, SerializationHelper
        );
    }

    // 清理
    ecs.delete_entity(savehelper).expect("Cleanup crash");
}
}

那么,这里发生了什么?

  1. 我们首先创建一个新的组件类型 - SerializationHelper,它存储地图的副本(看,我们正在使用上面的地图内容!)。 然后它创建一个新实体,并为其提供新组件 - 以及地图的副本(clone 命令创建深层副本)。 这是必需的,这样我们就不需要单独序列化地图。
  2. 我们进入一个代码块以避免借用检查器问题。
  3. 我们将 data 设置为一个元组,其中包含 Entity 存储和 SimpleMarkerReadStorage。 这些将由保存宏使用。
  4. 我们在当前目录中打开一个名为 savegame.jsonFile
  5. 我们从 Serde 获取 JSON 序列化器。
  6. 我们使用所有类型调用 serialize_individually 宏。
  7. 我们删除我们创建的临时助手实体。

如果你 cargo run 并开始一个游戏,然后保存它 - 你会发现一个 savegame.json 文件已经出现 - 其中包含你的游戏状态。 耶!

恢复游戏状态

现在我们有了游戏数据,是时候加载它了!

是否有已保存的游戏?

首先,我们需要知道是否已保存的游戏要加载。 在 saveload_system.rs 中,我们添加以下函数:

#![allow(unused)]
fn main() {
pub fn does_save_exist() -> bool {
    Path::new("./savegame.json").exists()
}
}

然后在 gui.rs 中,我们扩展 main_menu 函数以检查文件是否存在 - 如果不存在则不提供加载选项:

pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
    let save_exists = super::saveload_system::does_save_exist();
    let runstate = gs.ecs.fetch::<RunState>();

    ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");

    if let RunState::MainMenu{ menu_selection : selection } = *runstate {
        if selection == MainMenuSelection::NewGame {
            ctx.print_color_centered(24, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Begin New Game");
        } else {
            ctx.print_color_centered(24, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Begin New Game");
        }

        if save_exists {
            if selection == MainMenuSelection::LoadGame {
                ctx.print_color_centered(25, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Load Game");
            } else {
                ctx.print_color_centered(25, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Load Game");
            }
        }

        if selection == MainMenuSelection::Quit {
            ctx.print_color_centered(26, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Quit");
        } else {
            ctx.print_color_centered(26, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Quit");
        }

        match ctx.key {
            None => return MainMenuResult::NoSelection{ selected: selection },
            Some(key) => {
                match key {
                    VirtualKeyCode::Escape => { return MainMenuResult::NoSelection{ selected: MainMenuSelection::Quit } }
                    VirtualKeyCode::Up => {
                        let mut newselection;
                        match selection {
                            MainMenuSelection::NewGame => newselection = MainMenuSelection::Quit,
                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::NewGame,
                            MainMenuSelection::Quit => newselection = MainMenuSelection::LoadGame
                        }
                        if newselection == MainMenuSelection::LoadGame && !save_exists {
                            newselection = MainMenuSelection::NewGame;
                        }
                        return MainMenuResult::NoSelection{ selected: newselection }
                    }
                    VirtualKeyCode::Down => {
                        let mut newselection;
                        match selection {
                            MainMenuSelection::NewGame => newselection = MainMenuSelection::LoadGame,
                            MainMenuSelection::LoadGame => newselection = MainMenuSelection::Quit,
                            MainMenuSelection::Quit => newselection = MainMenuSelection::NewGame
                        }
                        if newselection == MainMenuSelection::LoadGame && !save_exists {
                            newselection = MainMenuSelection::Quit;
                        }
                        return MainMenuResult::NoSelection{ selected: newselection }
                    }
                    VirtualKeyCode::Return => return MainMenuResult::Selected{ selected : selection },
                    _ => return MainMenuResult::NoSelection{ selected: selection }
                }
            }
        }
    }

    MainMenuResult::NoSelection { selected: MainMenuSelection::NewGame }
}

最后,我们将修改 main.rs 中的调用代码以调用游戏加载:

#![allow(unused)]
fn main() {
RunState::MainMenu{ .. } => {
    let result = gui::main_menu(self, ctx);
    match result {
        gui::MainMenuResult::NoSelection{ selected } => newrunstate = RunState::MainMenu{ menu_selection: selected },
        gui::MainMenuResult::Selected{ selected } => {
            match selected {
                gui::MainMenuSelection::NewGame => newrunstate = RunState::PreRun,
                gui::MainMenuSelection::LoadGame => {
                    saveload_system::load_game(&mut self.ecs);
                    newrunstate = RunState::AwaitingInput;
                }
                gui::MainMenuSelection::Quit => { ::std::process::exit(0); }
            }
        }
    }
}
}

实际加载游戏

saveload_system.rs 中,我们将需要另一个宏! 这与 serialize_individually 宏非常相似 - 但反转了过程,并包含了一些细微的更改:

#![allow(unused)]
fn main() {
macro_rules! deserialize_individually {
    ($ecs:expr, $de:expr, $data:expr, $( $type:ty),*) => {
        $(
        DeserializeComponents::<NoError, _>::deserialize(
            &mut ( &mut $ecs.write_storage::<$type>(), ),
            &mut $data.0, // entities
            &mut $data.1, // marker
            &mut $data.2, // allocater
            &mut $de,
        )
        .unwrap();
        )*
    };
}
}

这是从一个新函数 load_game 调用的:

#![allow(unused)]
fn main() {
pub fn load_game(ecs: &mut World) {
    {
        // 删除所有内容
        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, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage,
            AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
            WantsToDropItem, SerializationHelper
        );
    }

    let mut deleteme : Option<Entity> = None;
    {
        let entities = ecs.entities();
        let helper = ecs.read_storage::<SerializationHelper>();
        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(); super::map::MAPCOUNT];
            deleteme = 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");
}
}

这真是有点拗口,所以让我们逐步了解它:

  1. 在一个代码块内(为了让借用检查器保持愉快),我们迭代游戏中的所有实体。 我们将它们添加到向量中,然后迭代该向量 - 删除实体。 这是一个两步过程,以避免在第一遍中使迭代器无效。
  2. 我们打开 savegame.json 文件,并附加一个 JSON 反序列化器。
  3. 然后我们为宏构建元组,这需要对实体存储进行可变访问,对标记存储进行写入访问,以及一个分配器(来自 Specs)。
  4. 现在我们将其传递给我们刚制作的宏,该宏依次为每种类型调用反序列化器。 由于我们以相同的顺序保存,它将拾取所有内容。
  5. 现在我们进入另一个代码块,以避免与之前的代码和实体删除发生借用冲突。
  6. 我们首先迭代所有具有 SerializationHelper 类型的实体。 如果我们找到它,我们将访问存储地图的资源 - 并替换它。 由于我们没有序列化 tile_content,因此我们将其替换为空向量集。
  7. 然后我们通过迭代具有 Player 类型和 Position 类型的实体来找到玩家。 我们存储玩家实体及其位置的世界资源。
  8. 最后,我们删除助手实体 - 这样如果我们再次保存游戏,就不会有重复项。

如果你现在 cargo run,你可以加载你保存的游戏了!

只是添加永久死亡!

如果我们让你在重新加载后保留你的保存游戏,那就不算是真正的 roguelike 了! 因此,我们将在 saveload_system 中添加另一个函数:

#![allow(unused)]
fn main() {
pub fn delete_save() {
    if Path::new("./savegame.json").exists() { std::fs::remove_file("./savegame.json").expect("Unable to delete file"); }
}
}

我们将在 main.rs 中添加一个调用,以便在加载游戏后删除保存:

#![allow(unused)]
fn main() {
gui::MainMenuSelection::LoadGame => {
    saveload_system::load_game(&mut self.ecs);
    newrunstate = RunState::AwaitingInput;
    saveload_system::delete_save();
}
}

Web Assembly

示例按原样编译并在 web assembly (wasm32) 平台上运行:但是一旦你尝试保存游戏,它就会崩溃。 不幸的是(嗯,如果你喜欢你的计算机不被你访问的每个网站攻击,那这其实是幸运的!),wasm 是沙盒化的 - 并且不具备在本地保存文件的能力。

支持通过 LocalStorage(浏览器/JavaScript 功能)进行保存计划在 RLTK 的未来版本中实现。 在此期间,我们将添加一些包装器以避免崩溃 - 并且只是在 wasm32 上实际上不保存游戏。

Rust 提供了条件编译(如果你熟悉 C,它很像你在大型跨平台库中找到的 #define 混乱情况)。 在 saveload_system.rs 中,我们将修改 save_game 以仅在非 web assembly 平台上编译:

#![allow(unused)]
fn main() {
#[cfg(not(target_arch = "wasm32"))]
pub fn save_game(ecs : &mut World) {
}

# 标签看起来有点吓人,但如果你展开它,它就很有意义。 #[cfg()] 表示“仅当当前配置与括号的内容匹配时才编译。 not() 反转检查的结果,因此当我们检查 target_arch = "wasm32")(我们是否正在为 wasm32 编译)时,结果被反转。 最终结果是,该函数仅在你wasm32 构建时才编译。

这完全没问题,但是有对该函数的调用 - 因此在 wasm 上的编译将失败。 我们将添加一个 桩函数 来代替它:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "wasm32")]
pub fn save_game(_ecs : &mut World) {
}
}

#[cfg(target_arch = "wasm32")] 前缀表示“仅为 web assembly 编译此代码”。 我们保持了函数签名相同,但在 _ecs 之前添加了 _ - 告诉编译器我们不打算使用该变量。 然后我们保持函数为空。

结果? 你可以为 wasm32 编译,save_game 函数只是根本不任何事情。 其余结构仍然存在,因此游戏正确返回到主菜单 - 但没有恢复功能。

(为什么检查文件是否存在有效? Rust 非常聪明,可以说“没有文件系统,所以文件不可能存在”。 谢谢,Rust!)

总结

这是一个很长的章节,内容相当繁重。 好消息是我们现在有了一个框架,可以随时加载和保存游戏。 添加组件增加了一些步骤:我们必须在 main 中注册它们,标记它们为 Serialize, Deserialize,并记住将它们添加到 saveload_system.rs 中的组件类型列表中。 这可能会更容易 - 但这是一个非常坚实的基础。

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

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


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