Save / Load state of grid engine
zewa666 opened this issue · 12 comments
Hey there @Annoraaq.
My game is progressing and I'm starting to think about creating a save/load feature. The most simple case I could think of would be to create a full dump of the games state along with an load feature to read a json to restore a state. So conceptually the workflow would be:
- To save: At any given state of the game run something like
this.gridEngine.save(opts?) -> json
. - To load: Refresh the game and instead of loading the gridengine as usual pass on additionally the save state
this.gridEngine.create(cloudCityTilemap, gridEngineConfig, jsonGameState);
Now the question is what type of information would be stored from GridEngine. As far as I understand it could be essentially everything related to Characters, or is there more to it? Also for multiple scenes I'd simply call save of every scene in order to get the gridEngine states of every "map"
Hey @zewa666. I can think of two complex things when storing the state of GridEngine. These are
- Ongoing movements like
moveTo()
- Unfinished movement animations
With ongoing movements I mean that if you want to save the game while a character is still moving towards a target position or is within an ongoing random movement step, then this information has to be stored as well.
With unfinished movement animations I mean that if you save the game while a character is inbetween two tiles, that has to be stored, too.
If you don't need to take care about the above two things, you could simply store all the characters tile positions when saving. And when loading just call gridEngine.create(...)
with the loaded character positions.
If you do need to take care about the above two points it gets a bit hairy. So before going down this rabbit hole, it would be good to know if this is really a requirement or if it is sufficient to just store the current positions. For ongoing movements you could probably keep track of them yourself whenever you start them and store them on saving.
A method to get the serialized state might be a good idea, though. Maybe as a first step it could only return the current positions and facing directions of the characters. In another iteration ongoing movements could be supported and in a third iteration the unfinished movement animations could be stored.
It seems to me that these iterations are ordered by descending usefulness and ascending complexity. Which means the first version is quite useful and easy to implement while the third iteration is probably only useful to some users and takes a lot of work.
good thinking. yeah I'm pretty sure for typical basic scenarios what, at least I would like to have, is the position of the character as well as their facing position. any movement would get the last full tile location. so if in between either the start or the next full tile the player is at. but I would be also totally fine skipping movement alltogether and acting as if it dodnt happen while a save occurs in between.
what came to my mind is how to handle sprites. would you export them as well or rely on them being provided on load
in the config?
just to give a bit more of insights. I'm currently introducing a state management library (aurelia-store) where I'd make sure to keep track of all my scene custom objects states (resources collected, ingame time ...). the current state would than be simply enriched with the gridEngine internals and than serialized into localstorage. on hydration I'd apply the initialState with the one from localStorage and tap into the gridEngine config to provide the load.
so nothing too spectacular and all scenes would get rebuilt as usual, only state updated afterwards
Regarding the sprites, I would not export them with GridEngine because GridEngine only uses sprites that you have to create outside of GridEngine. So the control over the sprites should not lie within GridEngine.
But it is an interesting question how the serialization method would deal with the sprites then, because GridEngine only has references to the sprites and these will not be valid anymore when "loading". Also, Phaser containers have to be considered in the same way.
When I think about this I am not sure anymore that a serialization method would be a good thing for GridEngine. There has to be some relation between characters and sprites that is not part of GridEngine anyway. That is somewhere in the logic of the actual game. And therefore the logic to save this relation should also be part of the game, I think.
GridEngine needs to make sure that you have an easy way to get the data that you need to initialize it. And with getAllCharacters()
, getPosition()
, getFacingDirection()
, getCollisionGroups()
, getCharLayer()
you should be able to get everything you need in order to recreate GridEngine after loading.
fair enough, but the trouble is with said methods its A cumbersome to get every individual state property and B not every state can be queried as far as I saw (speed, offset, ...)
so how instead of serialize/load a getState
and setState
method would work on primitives excluding PhaserObjects like sprites, tilemaps etc. this way I could:
- serialize my e.g NPC class, enriched with
gridEngineStates
- when loading as usual go through create, and than loop over characters and call setState with their value
EDIT:
alternatively, if you would expose the GridCharacter instances, something similar could be achieved on user side. the downside is of course that internals are exposed and prone to wrong/unintended use
not every state can be queried as far as I saw (speed, offset, ...)
You are right. That is something that should be added to GridEngine anyway.
Maybe a getCharacterState(...): CharacterState
method would be helpful then? It could give you the character state so you don't have to assemble it yourself with the individual methods. It could or could not include the sprite and container reference. Maybe it would be good to provide it. You can still remove it for serialization. But the getCharacterState()
method could also be used for other purposes where you need the sprite.
yep, that's what I meant with my getState
example. This way the contract between the users game and gridEngine continues to exist based on get/set methods. So it would be CharConfig & { missingPropsLikeFacingDirection }
? While I still think it would be good to separate references (everything Phaser...) and state (primitives except Position), as you mentioned the consumer can make sure to exclude not-desired props from serializatoin
Would you in this case also provide a setCharacterState
? and how would that react if Phaser references are included?
CharConfig
could also be extended by the facing direction (and whatever else is missing). Then CharacterState
could be replaced by CharConfig
. setCharacterState(...)
could also be useful. Maybe with Partial<CharacterConfig>
as an argument, so you can provide only those properties that you want to overwrite and the others will stay the same. I would say it can only be applied to an already existing character and is simply a more convenient but equivalent replacement to manually calling all the setters.
It would make saving and loading easier because you don't have to adapt your code if a new property is added in the future.
Agree with all points you made.
and staying future-proof is the exact reason I was heading for these helpers vs having to call individual methods.
just a quick look into the internals shows that the interface CharacterData
is actually the right interface to take as it's both fully documented, already used during create phase and includes all the properties necessary. As for the setCharacterState it would be Exclude<CharacterData, { id: string }>
as setter object
Sounds good.
About the setter object. How about making it Partial<CharacterData>
instead? setCharacterState()
can only be used on an existing character, so it is fine to have only optional properties, because the existing char already has values for everything. Moreover it keeps you from removing the id
property from the characterData object, which would be the case if you use it to set a state that you previously saved.
yep totally right. Certainly better ergonomics with Partial vs Except. My point with Except
was to indicate that setCharacterState
definitely should not set the id
, even if provided. But you're right that can be silently handled.