Loading and Saving the Game


About this tutorial

This tutorial is free and open source, and all code uses the MIT license - so you are free to do with it as you like. My hope is that you will enjoy the tutorial, and make great games!

If you enjoy this and would like me to keep writing, please consider supporting my Patreon.

Hands-On Rust


In the last few chapters, we've focused on getting a playable (if not massively fun) game going. You can run around, slay monsters, and make use of various items. That's a great start! Most games let you stop playing, and come back later to continue. Fortunately, Rust (and associated libraries) makes it relatively easy.

A Main Menu

If you're going to resume a game, you need somewhere from which to do so! A main menu also gives you the option to abandon your last save, possibly view credits, and generally tell the world that your game is here - and written by you. It's an important thing to have, so we'll put one together.

Being in the menu is a state - so we'll add it to the ever-expanding RunState enum. We want to include menu state inside it, so the definition winds up looking like this:


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

In gui.rs, we add a couple of enum types to handle main menu selections:


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

Your GUI is probably now telling you that main.rs has errors! It's right - we need to handle the new RunState option. We'll need to change things around a bit to ensure that we aren't also rendering the GUI and map when in the menu. So we rearrange 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);
            }
        }
    }
    ...
}

We'll also handle the MainMenu state in our large match for RunState:


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

We're basically updating the state with the new menu selection, and if something has been selected we change the game state. For Quit, we simply terminate the process. For now, we'll make loading/starting a game do the same thing: go into the PreRun state to setup the game.

The last thing to do is to write the menu itself. In 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 }
}

That's a bit of a mouthful, but it displays menu options and lets you select them with the up/down keys and enter. It's very careful to not modify state itself, to keep things clear.

Including Serde

Serde is pretty much the gold-standard for serialization in Rust. It makes a lot of things easier! So the first step is to include it. In your project's Cargo.toml file, we'll expand the dependencies section to include it:

[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"

It may be worth calling cargo run now - it will take a while, downloading the new dependencies (and all of their dependencies) and building them for you. It should keep them around so you don't have to wait this long every time you build.

Adding a "SaveGame" state

We'll extend RunState once more to support game saving:


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

In tick, we'll add dummy code for now:


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

In player.rs, we'll add another keyboard handler - escape:


#![allow(unused)]
fn main() {
// Save and Quit
VirtualKeyCode::Escape => return RunState::SaveGame,
}

If you cargo run now, you can start a game and press escape to quit to the menu.

Getting started with saving the game

Now that the scaffolding is in place, it's time to actually save something! Lets start simple, to get a feel for Serde. In the tick function, we extend the save system to just dump a JSON representation of the map to the console:


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

We'll also need to add an extern crate serde; to the top of main.rs.

This won't compile, because we need to tell Map to serialize itself! Fortunately, serde provides some helpers to make this easy. At the top of map.rs, we add use serde::{Serialize, Deserialize};. We then decorate the map to derive serialization and de-serialization code:


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

Note that we've decorated tile_content with directives to not serialize/de-serialize it. This prevents us from needing to store the entities, and since this data is rebuilt every frame - it doesn't matter. The game still won't compile; we need to add similar decorators to TileType and 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
}
}

If you cargo run the project now, when you hit escape it will dump a huge blob of JSON data to the console. That's the game map!

Saving entity state

Now that we've seen how useful serde is, we should start to use it for the game itself. This is harder than one might expect, because of how specs handles Entity structures: their ID # is purely synthetic, with no guaranty that you'll get the same one next time! Also, you may not want to save everything - so specs introduces a concept of markers to help with this. It winds up being a bit more of a mouthful than it really needs to be, but gives a pretty powerful serialization system.

Introducing Markers

First of all, in main.rs we'll tell Rust that we'd like to make use of the marker functionality:


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

In components.rs, we'll add a marker type:


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

Back in main.rs, we'll add SerializeMe to the list of things that we register:


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

We'll also add an entry to the ECS resources, which gets used to determine the next identity:


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

Finally, in spawners.rs we tell each entity builder to include the marker. Here's the complete entry for the 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()
}
}

The new line (.marked::<SimpleMarker<SerializeMe>>()) needs to be repeated for all of our spawners in this file. It's worth looking at the source for this chapter; to avoid making a huge chapter full of source code, I've omitted the repeated details.

The ConvertSaveload derive macro

The Entity class itself (provided by Specs) isn't directly serializable; it's actually a reference to an identity in a special structure called a "slot map" (basically a really efficient way to store data and keep the locations stable until you delete it, but re-use the space when it becomes available). So, in order to save and load Entity classes, it becomes necessary to convert these synthetic identities to unique ID numbers. Fortunately, Specs provides a derive macro called ConvertSaveload for this purpose. It works for most components, but not for all!

It's pretty easy to serialize a type that doesn't have an Entity in it - but does have data: mark it with #[derive(Component, ConvertSaveload, Clone)]. So we go through all the simple component types in components.rs; for example, here's Position:


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

So what this is saying is that:

  • The structure is a Component. You can replace this with writing code specifying Specs storage if you prefer, but the macro is much easier!
  • ConvertSaveload is actually adding Serialize and Deserialize, but with extra conversion for any Entity classes it encounters.
  • Clone is saying "this structure can be copied in memory from one point to another." This is necessary for the inner-workings of Serde, and also allows you to attach .clone() to the end of any reference to a component - and get another, perfect copy of it. In most cases, clone is really fast (and occasionally the compiler can make it do nothing at all!)

When you have a component with no data, the ConvertSaveload macro doesn't work! Fortunately, these don't require any additional conversion - so you can fall back to the default Serde syntax. Here's a non-data ("tag") class:


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

Actually saving something

The code for loading and saving gets large, so we've moved it into saveload_system.rs. Then include a mod saveload_system; in main.rs, and replace the SaveGame state with:


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

So... onto implementing save_game. Serde and Specs work decently together, but the bridge is still pretty roughly defined. I kept running into problems like it failing to compile if I had more than 16 component types! To get around this, I build a macro. I recommend just copying the macro until you feel ready to learn Rust's (impressive) macro system.


#![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();
        )*
    };
}
}

The short version of what it does is that it takes your ECS as the first parameter, and a tuple with your entity store and "markers" stores in it (you'll see this in a moment). Every parameter after that is a type - listing a type stored in your ECS. These are repeating rules, so it issues one SerializeComponent::serialize call per type. It's not as efficient as doing them all at once, but it works - and doesn't fall over when you exceed 16 types! The save_game function then looks like this:


#![allow(unused)]
fn main() {
pub fn save_game(ecs : &mut World) {
    // Create helper
    let mapcopy = ecs.get_mut::<super::map::Map>().unwrap().clone();
    let savehelper = ecs
        .create_entity()
        .with(SerializationHelper{ map : mapcopy })
        .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, CombatStats, SufferDamage, WantsToMelee, Item, Consumable, Ranged, InflictsDamage, 
            AreaOfEffect, Confusion, ProvidesHealing, InBackpack, WantsToPickupItem, WantsToUseItem,
            WantsToDropItem, SerializationHelper
        );
    }

    // Clean up
    ecs.delete_entity(savehelper).expect("Crash on cleanup");
}
}

What's going on here, then?

  1. We start by creating a new component type - SerializationHelper that stores a copy of the map (see, we are using the map stuff from above!). It then creates a new entity, and gives it the new component - with a copy of the map (the clone command makes a deep copy). This is needed so we don't need to serialize the map separately.
  2. We enter a block to avoid borrow-checker issues.
  3. We set data to be a tuple, containing the Entity store and ReadStorage for SimpleMarker. These will be used by the save macro.
  4. We open a File called savegame.json in the current directory.
  5. We obtain a JSON serializer from Serde.
  6. We call the serialize_individually macro with all of our types.
  7. We delete the temporary helper entity we created.

If you cargo run and start a game, then save it - you'll find a savegame.json file has appeared - with your game state in it. Yay!

Restoring Game State

Now that we have the game data, it's time to load it!

Is there a saved game?

First, we need to know if there is a saved game to load. In saveload_system.rs, we add the following function:


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

Then in gui.rs, we extend the main_menu function to check for the existence of a file - and not offer to load it if it isn't there:

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 }
}

Finally, we'll modify the calling code in main.rs to call game loading:


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

Actually loading the game

In saveload_system.rs, we're going to need another macro! This is pretty much the same as the serialize_individually macro - but reverses the process, and includes some slight changes:


#![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();
        )*
    };
}
}

This is called from a new function, load_game:


#![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, 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");
}
}

That's quite the mouthful, so lets step through it:

  1. Inside a block (to keep the borrow checker happy), we iterate all entities in the game. We add them to a vector, and then iterate the vector - deleting the entities. This is a two-step process to avoid invalidating the iterator in the first pass.
  2. We open the savegame.json file, and attach a JSON deserializer.
  3. Then we build the tuple for the macro, which requires mutable access to the entities store, write access to the marker store, and an allocator (from Specs).
  4. Now we pass that to the macro we just made, which calls the de-serializer for each type in turn. Since we saved in the same order, it will pick up everything.
  5. Now we go into another block, to avoid borrow conflicts with the previous code and the entity deletion.
  6. We first iterate all entities with a SerializationHelper type. If we find it, we get access to the resource storing the map - and replace it. Since we aren't serializing tile_content, we replace it with an empty set of vectors.
  7. Then we find the player, by iterating entities with a Player type and a Position type. We store the world resources for the player entity and his/her position.
  8. Finally, we delete the helper entity - so we won't have a duplicate if we save the game again.

If you cargo run now, you can load your saved game!

Just add permadeath!

It wouldn't really be a roguelike if we let you keep your save game after you reload! So we'll add one more function to 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"); } 
}
}

We'll add a call to main.rs to delete the save after we load the game:


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

Web Assembly

The example as-is will compile and run on the web assembly (wasm32) platform: but as soon as you try to save the game, it crashes. Unfortunately (well, fortunately if you like your computer not being attacked by every website you go to!), wasm is sandboxed - and doesn't have the ability to save files locally.

Supporting saving via LocalStorage (a browser/JavaScript feature) is planned for a future version of RLTK. In the meantime, we'll add some wrappers to avoid the crash - and simply not actually save the game on wasm32.

Rust offers conditional compilation (if you are familiar with C, it's a lot like the #define madness you find in big, cross-platform libraries). In saveload_system.rs, we'll modify save_game to only compile on non-web assembly platforms:


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

That # tag is scary looking, but it makes sense if you unwrap it. #[cfg()] means "only compile if the current configuration matches the contents of the parentheses. not() inverts the result of a check, so when we check that target_arch = "wasm32") (are we compiling for wasm32) the result is inverted. The end result of this is that the function only compiles if you aren't building for wasm32.

That's all well and good, but there are calls to that function - so compilation on wasm will fail. We'll add a stub function to take its place:


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

The #[cfg(target_arch = "wasm32")] prefix means "only compile this for web assembly". We've kept the function signature the same, but added a _ before _ecs - telling the compiler that we intend not to use that variable. Then we keep the function empty.

The result? You can compile for wasm32 and the save_game function simply doesn't do anything at all. The rest of the structure remains, so the game correctly returns to the main menu - but with no resume function.

(Why does the check that the file exists work? Rust is smart enough to say "no filesystem, so the file can't exist". Thanks, Rust!)

Wrap-up

This has been a long chapter, with quite heavy content. The great news is that we now have a framework for loading and saving the game whenever we want to. Adding components has gained some steps: we have to register them in main, tag them for Serialize, Deserialize, and remember to add them to our component type lists in saveload_system.rs. That could be easier - but it's a very solid foundation.

The source code for this chapter may be found here

Run this chapter's example with web assembly, in your browser (WebGL2 required)


Copyright (C) 2019, Herbert Wolverson.