Skip to content

Commit f4b476c

Browse files
committed
amethyst#50 - Improved chapter 11, using the ConvertSaveload macro rather than writing manual serialization helpers. Much nicer code and easier chapter. Still need to port it to all future chapters (which will take a while).
1 parent c9eeda6 commit f4b476c

File tree

4 files changed

+50
-215
lines changed

4 files changed

+50
-215
lines changed

.vscode/spellright.dict

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ Crocodylus
4646
Moresmau
4747
fut
4848
Asterix
49+
ConvertSaveload

book/src/chapter_11.md

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ Being in the menu is a *state* - so we'll add it to the ever-expanding `RunState
2020

2121
```rust
2222
#[derive(PartialEq, Copy, Clone)]
23-
pub enum RunState { AwaitingInput,
24-
PreRun,
25-
PlayerTurn,
26-
MonsterTurn,
27-
ShowInventory,
28-
ShowDropItem,
23+
pub enum RunState { AwaitingInput,
24+
PreRun,
25+
PlayerTurn,
26+
MonsterTurn,
27+
ShowInventory,
28+
ShowDropItem,
2929
ShowTargeting { range : i32, item : Entity},
3030
MainMenu { menu_selection : gui::MainMenuSelection }
3131
}
@@ -51,7 +51,7 @@ fn tick(&mut self, ctx : &mut Rltk) {
5151
newrunstate = *runstate;
5252
}
5353

54-
ctx.cls();
54+
ctx.cls();
5555

5656
match newrunstate {
5757
RunState::MainMenu{..} => {}
@@ -71,7 +71,7 @@ fn tick(&mut self, ctx : &mut Rltk) {
7171
}
7272

7373
gui::draw_ui(&self.ecs, ctx);
74-
}
74+
}
7575
}
7676
}
7777
...
@@ -104,7 +104,7 @@ pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
104104
let runstate = gs.ecs.fetch::<RunState>();
105105

106106
ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");
107-
107+
108108
if let RunState::MainMenu{ menu_selection : selection } = *runstate {
109109
if selection == MainMenuSelection::NewGame {
110110
ctx.print_color_centered(24, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Begin New Game");
@@ -181,12 +181,12 @@ We'll extend `RunState` once more to support game saving:
181181

182182
```rust
183183
#[derive(PartialEq, Copy, Clone)]
184-
pub enum RunState { AwaitingInput,
185-
PreRun,
186-
PlayerTurn,
187-
MonsterTurn,
188-
ShowInventory,
189-
ShowDropItem,
184+
pub enum RunState { AwaitingInput,
185+
PreRun,
186+
PlayerTurn,
187+
MonsterTurn,
188+
ShowInventory,
189+
ShowDropItem,
190190
ShowTargeting { range : i32, item : Entity},
191191
MainMenu { menu_selection : gui::MainMenuSelection },
192192
SaveGame
@@ -326,56 +326,33 @@ pub fn player(ecs : &mut World, player_x : i32, player_y : i32) -> Entity {
326326

327327
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.
328328

329-
## Serializing components that don't contain an Entity
329+
## The ConvertSaveload derive macro
330+
331+
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!
330332

331-
It's pretty easy to serialize a type that doesn't have an Entity in it: mark it with `#[derive(Component, Serialize, Deserialize, Clone)]`. So we go through all the simple component types in `components.rs`; for example, here's `Position`:
333+
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`:
332334

333335
```rust
334-
#[derive(Component, Serialize, Deserialize, Clone)]
336+
#[derive(Component, ConvertSaveload, Clone)]
335337
pub struct Position {
336338
pub x: i32,
337339
pub y: i32,
338340
}
339341
```
340342

341-
## Serializing components that point to entities
343+
So what this is saying is that:
342344

343-
Here is where it gets a little messy. There are no provided `derive` functions for handling serialization of `Entity`, so we have to do it the hard way. The good news is that we're not doing it very often. Here's a helper for `InBackpack`:
345+
* The structure is a `Component`. You can replace this with writing code specifying Specs storage if you prefer, but the macro is much easier!
346+
* `ConvertSaveload` is actually adding `Serialize` and `Deserialize`, but with extra conversion for any `Entity` classes it encounters.
347+
* `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!)
344348

345-
```rust
346-
// InBackpack wrapper
347-
#[derive(Serialize, Deserialize, Clone)]
348-
pub struct InBackpackData<M>(M);
349-
350-
impl<M: Marker + Serialize> ConvertSaveload<M> for InBackpack
351-
where
352-
for<'de> M: Deserialize<'de>,
353-
{
354-
type Data = InBackpackData<M>;
355-
type Error = NoError;
356-
357-
fn convert_into<F>(&self, mut ids: F) -> Result<Self::Data, Self::Error>
358-
where
359-
F: FnMut(Entity) -> Option<M>,
360-
{
361-
let marker = ids(self.owner).unwrap();
362-
Ok(InBackpackData(marker))
363-
}
349+
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:
364350

365-
fn convert_from<F>(data: Self::Data, mut ids: F) -> Result<Self, Self::Error>
366-
where
367-
F: FnMut(M) -> Option<Entity>,
368-
{
369-
let entity = ids(data.0).unwrap();
370-
Ok(InBackpack{owner: entity})
371-
}
372-
}
351+
```rust
352+
#[derive(Component, Serialize, Deserialize, Clone)]
353+
pub struct Player {}
373354
```
374355

375-
So we start off by making a "data" class for `InBackpack`, which simply stores the entity at which it points. Then we implement `convert_info` and `convert_from` to satisfy Specs' `ConvertSaveLoad` trait. In `convert_into`, we use the `ids` map to get a saveable ID number for the item, and return an `InBackpackData` using this marker. `convert_from` does the reverse: we get the ID, look up the ID, and return an `InBackpack` method.
376-
377-
So that's not *too* bad. If you look at the source, we've done this for all of the types that store `Entity` data - some of which have other data, or multiple `Entity` types.
378-
379356
## Actually saving something
380357

381358
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:
@@ -435,7 +412,7 @@ pub fn save_game(ecs : &mut World) {
435412
}
436413
```
437414

438-
What's going on here, then?
415+
What's going on here, then?
439416

440417
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.
441418
2. We enter a block to avoid borrow-checker issues.
@@ -469,7 +446,7 @@ pub fn main_menu(gs : &mut State, ctx : &mut Rltk) -> MainMenuResult {
469446
let runstate = gs.ecs.fetch::<RunState>();
470447

471448
ctx.print_color_centered(15, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "Rust Roguelike Tutorial");
472-
449+
473450
if let RunState::MainMenu{ menu_selection : selection } = *runstate {
474451
if selection == MainMenuSelection::NewGame {
475452
ctx.print_color_centered(24, RGB::named(rltk::MAGENTA), RGB::named(rltk::BLACK), "Begin New Game");

0 commit comments

Comments
 (0)