Saved Games
Saved game is used to store progress made in a play-through of a game to disk or some other storage. It is very important for pretty much every game and this chapter will help you to understand basic concepts of saved games in the engine.
Saved Game Structure
This could sound weird, but saved game in most cases is just a scene with additional data. Let's understand why. At first, when you're making a save file you need to take some sort of "snapshot" of your game world. Essential way of storing such data is a scene. Secondly, game plugins is also may store some data that should be saved. By these two facts, it is quite easy to get a full picture: to make a save all you need to do is to serialize current scene, serialize some other data and just "dump" it to a file. You might ask: is this efficient to serialize the entire scene? In short: yes. A bit more detailed answer: when you serialize a scene, it does not store everything, it only stores changed fields and references to external assets.
Usage
Fyrox offers a built-in system for saved games. It does exactly what said in the section above - serializes a "diff" of your scene which can be loaded later as an ordinary scene and the engine will do all the magic for you. Typical usage of this system is very simple:
#![allow(unused)] fn main() { #[derive(Visit, Reflect, Debug, Default)] struct MyGame { scene: Handle<Scene>, } impl MyGame { fn new(scene_path: Option<&str>, context: PluginContext) -> Self { // Load the scene as usual. context .async_scene_loader .request(scene_path.unwrap_or("data/scene.rgs")); Self { scene: Handle::NONE, } } fn save_game(&mut self, context: &mut PluginContext) { let mut visitor = Visitor::new(); // Serialize the current scene. context.scenes[self.scene] .save("Scene", &mut visitor) .unwrap(); // Save it to a file. visitor.save_binary(Path::new("save.rgs")).unwrap() } fn load_game(&mut self, context: &mut PluginContext) { // Loading of a saved game is very easy - just ask the engine to load your save file. // Note the difference with `Game::new` - here we use `request_raw` instead of // `request` method. The main difference is that `request` creates a derived scene // from a source scene, but `request_raw` loads the scene without any modifications. context.async_scene_loader.request_raw("save.rgs"); } } impl Plugin for MyGame { fn on_scene_begin_loading(&mut self, _path: &Path, context: &mut PluginContext) { if self.scene.is_some() { context.scenes.remove(self.scene); } } fn on_scene_loaded( &mut self, _path: &Path, scene: Handle<Scene>, _data: &[u8], _context: &mut PluginContext, ) { self.scene = scene; } } }
This is a typical structure of a game that supports saving and loading. As you can see, it is pretty much the same as
the standard code, that can be generated by fyrox-template
. The main difference here is two new methods with
self-describing names: save_game
and load_game
. Let's try to understand what each one does.
save_game
serializes your current game scene into a file. This function is very simple and can be used as-is in
pretty much any game. You can also write additional game data here using the visitor
instance (see next section).
load_game
- loads a saved game. It just asks the engine to load your save file as an ordinary scene. Note the difference
with code in Game::new
- here we use request_raw
instead of request
method. The main difference is that request
creates a derived scene from a source scene, but request_raw
loads the scene without any modifications. What is
derived scene anyway? It is a scene, which does not store all the required data inside, instead, it stores links to
places where the data can be obtained from. You can also think of it as a difference between your saved game and an
original scene.
You can bind these two functions to some keys, for example you can use F5
for save and F9
for load and call the
respective methods for saving/loading. Also, these methods could be used when a button was pressed, etc.
Additional Data
As was mentioned in the previous section, it is possible to store additional data in a saved game. It is very simple to do:
#![allow(unused)] fn main() { #[derive(Visit, Reflect, Debug, Default)] struct MyData { foo: String, bar: u32, } #[derive(Visit, Reflect, Debug, Default)] struct MyGame { scene: Handle<Scene>, data: MyData, } impl MyGame { fn new(scene_path: Option<&str>, context: PluginContext) -> Self { // Load the scene as usual. context .async_scene_loader .request(scene_path.unwrap_or("data/scene.rgs")); Self { scene: Handle::NONE, data: Default::default(), } } fn save_game(&mut self, context: &mut PluginContext) { let mut visitor = Visitor::new(); // Serialize the current scene. context.scenes[self.scene] .save("Scene", &mut visitor) .unwrap(); // Write additional data. self.data.visit("Data", &mut visitor).unwrap(); // Save it to a file. visitor.save_binary(Path::new("save.rgs")).unwrap() } pub fn load_game(&mut self, context: &mut PluginContext) { // Loading of a saved game is very easy - just ask the engine to load your scene. // Note the difference with `Game::new` - here we use `request_raw` instead of // `request` method. The main difference is that `request` creates a derived scene // from a source scene, but `request_raw` loads the scene without any modifications. context.async_scene_loader.request_raw("save.rgs"); } } impl Plugin for MyGame { fn on_scene_begin_loading(&mut self, _path: &Path, context: &mut PluginContext) { if self.scene.is_some() { context.scenes.remove(self.scene); } } fn on_scene_loaded( &mut self, _path: &Path, scene: Handle<Scene>, data: &[u8], _context: &mut PluginContext, ) { self.scene = scene; // Restore the data when the scene was loaded. if let Ok(mut visitor) = Visitor::load_from_memory(data) { self.data.visit("Data", &mut visitor).unwrap(); } } } }
The main difference here with the code snippet from the previous section is that now we have MyData
structure which
we want to save in a save file as well as current scene. We're doing that in save_game
method by
self.data.visit("Data", &mut visitor).unwrap();
which serializes our data. To load the data back (deserialize), we
have to wait until the scene is fully loaded and then try to deserialize the data. This is done by the last three lines
of code of on_scene_loaded
method.