Many games require only simple saving and loading functionality. Perhaps all you need to save about a player is what level they’re up to and their high score — in which case you don’t need an article like this.
Other games have a moderately difficult time saving game state. These games, like The Legend of Zelda, require saving lots of information about the player – your level, experience, items, progress through quests – but only very limited data about the world (whether a secret was uncovered). If that describes your game, read this article just in case.
Other games still have a terrible time saving state. Not only do they need to save the player in the same manner as above, but also need to save loads of data about the world as well. Which dungeons and enemies are cleared, how the world was damaged or affected by the player, which items were bought or traded or left in other locations, etc. The Botanist is one of those games. Let’s read on:
Saving data in general
Let’s start with the simplest case in the introduction: saving just a few bits of game information. The question is not really how to get at the data, which is easy to just “pluck” out of the game manually in the
save() method. The question is: where to save the data? With Phaser.js, and essentially any HTML5/JS/Canvas/WebGL game, you basically have two viable options: 1) in localStorage or 2) on a centralized server.
localStorage is where we’ll start. The Botanist will eventually save to a centralized server so you can hop between your laptop and your phone, for instance, but we have to start somewhere and I’m leaving the backend out of this for now.
Be aware of the limitations of localStorage:
1) localStorage can only store strings, so you have to
JSON.stringify() everything before storage and
JSON.parse() when you read it back.
2) localStorage is a key/value store only. It is possible but annoying to run very simple queries against data you have in localStorage, if it’s planned for and structured in advance. It is not feasible to run complex queries against your data. (I mean, you can, but do it as an experiment, not in a game.)
3) There’s a 10MB storage limit in localStorage before the user is asked to allot more storage space to your app
4) It persists only to the browser - deleting “site data” (sometimes lumped in with “deleting cookies”) will delete the save, and you’ll not be able to access your save from a different computer
The upside is that localStorage is supported everywhere that canvas is, it’s super simple to use, and we’re ok with all those limitations (for now…)
The localStorage API is dead simple:
In the most simple case, you could do the following:
We’ll try to save everything under one key, though, so we’ll build an object :
Saving a player
Let’s move on to the second case above: saving a player. I wish we could just
JSON.stringify the player object, but it’s so complex and has so many internally used variables, objects, methods, and circular references that this is impossible.
Instead I’ve structured my player class - in fact, my entire character base class, which will come in handy later - to be “easy to serialize”. I’ve organized the custom properties, options, and methods that define each character’s behavior into a handful of different semantic groups. I’ve made sure that there are no circular references, and that any callback methods are specified by name rather than a closure. (The callback names are resolved at runtime so there’s no fuss with serialization).
This makes it easy, trivial almost, to define simple and sensible serialize and unserialize methods. Serialize just builds an object from a handful of keys, and unserialize restores them.
Now your game’s save method can simply store the serialized player, and unserialize the player on load. You can also add any other cherry picked game-level keys (like high score, or unlocked levels) to the object you’re saving, like so:
Note that if you only have one Character or Player class, you don’t need to deal with
className or a factory pattern. But some design decisions result in multiple child classes that need to be dealt with.
In The Botanist’s case, we use different classes for different character types. We do this so that different character types can have different scripted behavior, but all still inherit from the same base character class that does things like manage inventory or do battle.
The Botanist therefore uses a “Factory” pattern. A Factory is simply a helper method that allows you to create a class from its class name stored in string form. You can accomplish this quite simply:
But, of course, you’ll generally want to add some extra logic around validation and initialization.
This Factory method makes it easy for us to serialize lots of different types of NPCs and enemies. We need nothing more than the class name and the stored properties to fully unserialize any of our various character types.
Saving the entire game
What I’ve written so far covers probably 90% of app store games, since most games are built narcissistically around the player or main character - it makes sense that the information we need to save is almost entirely in the player object.
But some games need to save state for more than just the player. They need to save state for all the NPCs, all the maps and dungeons, all the items in the game. This is our third scenario in the introduction, and the case for The Botanist.
The first step is to identify which types of data need to be saved. Phrased differently: decide which classes will need serialize and unserialize methods. In The Botanist, the following classes have serialize methods:
- Character (parent class of both Player, Enemy, and NPC alike)
- Stage (or “map”, which manages the NPCs, Enemies, and Items in its game area)
- Game (which, of course, stores all of the above)
Serializing a Stage is almost as easy as serializing a character. We look through the important properties taking copies of them, with the consideration that arrays of NPCs and items need to be iterated through in order to have their members individually serialized. A one-line map call takes care of this, otherwise Stage’s
serialize method looks the same as Character’s. (We could abstract this behavior into, say, a Collection class, or extend Phaser’s Group class to add a serialize method, but I’m lazy.)
We can also avoid nested-string-hell by
JSON.parseing each item. Above,
Now that everything’s an object, rather than nested strings, the one
JSON.stringify call at the Game level will collapse the whole thing down into one piece of JSON. This is a tiny nit-picky optimization and doesn’t really matter, but I like it.
Our Item class is easier to serialize (rather, unserialize) than Characters and Stages as we only have one item class and don’t need to bother with a factory pattern or storing the class name. The Item class’
serialize method therefore looks exactly the same as Character’s (with different properties obviously), and the
Unserialize class method for Item is simpler in that we don’t call
Item.Factory but rather, simply
Finally, the Game class’ save and load methods orchestrate the whole thing. (PSA: It’s a good idea to “namespace” the data you’re committing to localStorage, just in case you ever have to embed your game in a third party site who may also use localStorage. Just prefix all the localStorage item keys you use with your app’s name.)
The Game class also has serialize and unserialize methods. The difference between
save is an important semantic difference: “serialize” means “turn into a string representation” whereas “save” means “actually do the saving”. I won’t show you
serialize here because by now you should know how to write that method for the Game class – it’s the same as the above. But here’s save and load:
You can see above that the save and load methods take a save name, that defaults to “default”. This approach lets users play different saves in the future, but also gives us a sensible default behavior.
And we get a bonus: autosave! It’s just save on an interval, with the save key “autosave”. Loading an autosave is accomplished by running
Finally, if you have a game even bigger than this that requires saving many more things: you can create a base “Serializable” class that reads from the instance’s “serializeFields” property. The class’
serialize method can then recursively iterate through all its children, automatically serializing any child object that has a
serialize method. Use this approach in only the most dire of situations.