加载和保存游戏
关于本教程
本教程是免费且开源的,所有代码均使用 MIT 许可证 - 因此您可以随意使用它。 我希望您会喜欢本教程,并制作出色的游戏!
如果您喜欢这个教程并希望我继续写作,请考虑支持我的 Patreon。
在前几个章节中,我们专注于让游戏变得可玩(即使不是非常有趣)。 你可以四处奔跑,杀死怪物,并使用各种物品。 这是一个好的开始! 大多数游戏都允许你停止游玩,并在稍后返回以继续。 幸运的是,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
,使其不进行序列化/反序列化。 这可以防止我们需要存储实体,并且由于此数据在每一帧都会重建 - 因此无关紧要。 游戏仍然无法编译; 我们需要向 TileType
和 Rect
添加类似的装饰器:
#![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 提供了一个名为 ConvertSaveload
的 derive
宏来实现此目的。 它适用于大多数组件,但并非所有组件!
序列化不包含 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
实际上添加了Serialize
和Deserialize
,但是为它遇到的任何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"); } }
那么,这里发生了什么?
- 我们首先创建一个新的组件类型 -
SerializationHelper
,它存储地图的副本(看,我们正在使用上面的地图内容!)。 然后它创建一个新实体,并为其提供新组件 - 以及地图的副本(clone
命令创建深层副本)。 这是必需的,这样我们就不需要单独序列化地图。 - 我们进入一个代码块以避免借用检查器问题。
- 我们将
data
设置为一个元组,其中包含Entity
存储和SimpleMarker
的ReadStorage
。 这些将由保存宏使用。 - 我们在当前目录中打开一个名为
savegame.json
的File
。 - 我们从 Serde 获取 JSON 序列化器。
- 我们使用所有类型调用
serialize_individually
宏。 - 我们删除我们创建的临时助手实体。
如果你 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"); } }
这真是有点拗口,所以让我们逐步了解它:
- 在一个代码块内(为了让借用检查器保持愉快),我们迭代游戏中的所有实体。 我们将它们添加到向量中,然后迭代该向量 - 删除实体。 这是一个两步过程,以避免在第一遍中使迭代器无效。
- 我们打开
savegame.json
文件,并附加一个 JSON 反序列化器。 - 然后我们为宏构建元组,这需要对实体存储进行可变访问,对标记存储进行写入访问,以及一个分配器(来自 Specs)。
- 现在我们将其传递给我们刚制作的宏,该宏依次为每种类型调用反序列化器。 由于我们以相同的顺序保存,它将拾取所有内容。
- 现在我们进入另一个代码块,以避免与之前的代码和实体删除发生借用冲突。
- 我们首先迭代所有具有
SerializationHelper
类型的实体。 如果我们找到它,我们将访问存储地图的资源 - 并替换它。 由于我们没有序列化tile_content
,因此我们将其替换为空向量集。 - 然后我们通过迭代具有
Player
类型和Position
类型的实体来找到玩家。 我们存储玩家实体及其位置的世界资源。 - 最后,我们删除助手实体 - 这样如果我们再次保存游戏,就不会有重复项。
如果你现在 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.