idanarye / bevy-yoleck

Your Own Level Editor Creation Kit
Other
172 stars 11 forks source link

Saving and loading game state #23

Open idanarye opened 1 year ago

idanarye commented 1 year ago

The common conception about saving and loading game state in Bevy (or ECS in general) is that you just have to do serialize all the components of all the entities into a file, and when loading just rebuild the scene from that component data. That, however, is very similar to the common conception about editors for Bevy - the very conception that Yoleck is trying to be an alternative to. So I figure Yoleck can offer a similar alternative to. Since Yoleck already has a concise representation of the level, one that does not have to store every single component because it can build them from YoleckEntityTypes and YoleckComponents, it shouldn't be that much more work to save just the changed data.

Advantages over entire-scene save:

  1. Smaller files - has less data so save.
  2. Saves have a much better chance to survive changes to the levels and the game code. Of course, this is not something that can be guaranteed, and I'll need to come up with guidelines to which changes break the saves and which doesn't.
  3. During playtests in the editor, we will be able to save the state, modify the level, and then resume it from the same state.

Yoleck itself is not going to handle the storage medium for the saved files. Doing that will lock users to a specific persistence solution. Instead, when initiating a save Yoleck will send an event with a serde_json::Value that represents the state, and when loading the user will have to provide that serde_json::Value. Note that serde_json::Value is not an actual JSON - it's just an in-memory data type with JSON semantics. It can be stored as a more compact and less human-readable format.

idanarye commented 1 year ago

The general idea:

Saving be triggered by sending a YoleckInitiateSave event, which will make Yoleck run a YoleckSchedule::Save schedule where save systems will generate YoleckComponents based on the current state of the actual components. I'll need to decide exactly how to do that, but they probably won't be modifying the original YoleckComponents that the data was created from. After the schedule is done, a YoleckSaveReady event will be sent with the save data as serde_json::Value, and it'd be up to game code to persist it.

YoleckInitiateSave will have a context field (need to decide if it'd be a Box<dyn Any> or a serde_json::Value) which YoleckSaveReady will get (and edit systems can use as a resource). That field could be used by the game code to determine, for example, if the save is an autosave or user-invoked, and the name or number of the save in the later case.

As for loading - just add new YoleckLoadingCommand variants (or add an Option to the existing ones) with the serde_json::Value from the save data.

idanarye commented 1 year ago

Each entity type will have to declare its save strategy. I imagine three strategies (names TBD):

  1. Unsaved entity types. Example - walls. These will be loaded from the .yol file every time the level is loaded, and they never change so there is no need to put them in the save file. Actually, even if they do change this is a type of entity that it's fine not to save. For example - the pots in Zelda games. You can destroy them, but when you reenter the level they get recreated. This will be the default save strategy.
  2. Entities saved by identifier. Maybe the UUID from #21 - there is no need to maintain two UUIDs for the same entity. When loading a save, Yoleck will use the YoleckComponents of the entity from the save data to override the ones from the .yol file.
  3. Entities saved without an identifier. Since we cannot match them to entities from the .yol file, no entities of these types will be created at all when loading a saved level (only when loading that level for the very first time), and instead Yoleck will use the list of the se entities from the save data. Note that unlike the second strategy, where only the YoleckComponents that can change during gameplay need to be saved, here all the YoleckComponents need to be saved because we can't match them to entities from a .yol file.

I'm tempted to just have a single boolean field on the entity type (unsaved/saved) and distinguish between the second and third strategy by the existence or lack of the identifier component. But I think I'll make them explicit (with a 3-options enum) anyway, because the third strategy is kind of dangerous - using it would mean that new entities added to the level will be ignored when the level is loaded from a save. Also, this third strategy will probably be useful for Yoleck entities created on the fly (I should open a new ticket for that), and we may want to use UUIDs for these kind of entities (e.g. - a spawned monster and wanting to save some other entity that refers to that monster)