Contents
The demo used to illustrate this tutorial consists in a small clicker game, where you earn points by clicking on the Phaser logo. Your score is saved, allowing you to leave, come back and resume from where you left. In addition, your number of visits to the game is saved as well, in order to illustrate the concepts with more than one example. It’s obviously not a very interesting game but its simplicity allows to focus on the saving and loading of the data.
Before diving into implementation details, let’s review our options when it comes to saving progress and why localStorage seems like a good option.
Possible ways to save player progress
1. Cookies
The first thing that may come to your mind are cookies. One issue is that cookie data is sent to the server as part of each HTTP header. If the data you store is only relevant for your client-side code, sending it over is a waste of bandwidth (admittedly small, but still). In addition, cookies suffer from security problems and don’t easily allow to store complex data. Finally, European legislation has just made cookies a bit more cumbersome to use than in the past, due to the mandatory disclaimer that needs to be displayed!
2. Central database
Using a database allows you to centralize player data, access it and even modify it at will. Conversely, it prevents the players from modifying that data (providing that you check properly the data received from them), which might be important for some types of games. However, for small settings (like interface preferences), it may be a bit overkill. Also, you need to set up a secure way for the players to identify themselves before accessing the stored data, and you need a server to handle all of this.
3. WebStorage
WebStorage consists in two different types of storage: localStorage and sessionStorage. Both save data per domain in the browser. They offer up to 10Mb of storage (customizable by the user) which is cleanly accessible programmatically using javascript (and it just so happens that Phaser 3 is javascript as well!). Additionally, no information is sent to the server. While sessionStorage data expires at the end of the session, localStorage data persists indefinitely, until cleared by the user or by your program. Possibly the only downside is that users are free to modify this data at will, which makes it unsuitable for some types of games.
4. Local database: IndexedDb
IndexedDB provides an entire database environment locally, in the browser, allowing to save 50Mb of arbitrarily complex data using a complex API. This can be a very good option. However, in practice it may be a bit overkill as well. It is likely that use cases requiring a database will actually tend to use a central, server-side database instead, especially in the case of games. For more traditional apps however, this may be the best option.
At the end of this quick overview, I would summarize your options as follows:
- Could the game be adversely affected if the player could modify the stored data (i.e. cheating, in a way at affects other players for example)? Then you need to resort to a central database (which is not covered in this article).
- For all other cases (client-side single-player games essentially), the ideal option seems to be localStorage (or IndexedDB if you want to go wild on the data you store).
Now let’s see how saving to and loading from localStorage is performed in the demo.
Implementation
Saving data
Each time the score changes, the new value is immediately saved. This happens in Game.updateScore()
, which is itself called by the pointerdown
callback attached to the Phaser logo. Saving the data is done in one single line:
localStorage.setItem('score',Game.scene.score);
You can essentially view localStorage as a mapping between string keys to string values. Here, this line has the effect of binding the value of Game.scene.score
to the score
key in the localStorage of your browser.
The visits counter is updated only once, when the game loads. This happens in Game.setVisits()
.
Loading data
When loading the game, Game.setScore()
is called and fetches the data from localStorage. This is done with the following line:
Game.scene.score = parseInt(localStorage.getItem('score')) || 0;
Using the key score
, we fetch the stored value. If no value is found, we set it to 0. Pay attention to the use of parseInt()
! Remember that localStorage saves everything as strings. Usually, storing numbers as strings might not be a problem, but it may lead to much less obvious bugs if you store booleans for example (because the strings “true” and “false” both evaluate to the same boolean value when coerced!). I’ve personally lost quite some time on such bugs in the past. Bottom line: always parse what you get from localStorage.
Similarly, the number of visits is fetched from Game.setVisits()
.
Using save “files”
The above examples show how to save and load individual variables from localStorage. If for some reasons you’d like to create save “files”, that is, individual objects containing all the saved values, the most straightforward way to do so is by stringifying and parsing JSON objects, using JSON.stringify()
and JSON.parse()
respectively, and storing them as one single field in localStorage. This functionality is not used in the demo, but the code to do so is there nevertheless, and reproduced below:
Game.saveFile = function(){
var file = {
score: Game.scene.score,
visits: Game.scene.visits
};
localStorage.setItem('saveFile',JSON.stringify(file));
};
Game.loadFile = function(){
var file = JSON.parse(localStorage.getItem('saveFile'));
Game.scene.score = file.score;
Game.scene.visits = file.visits;
};
And that’s it, with this you should be able to use localStorage for your game. If you have any questions or remarks, feel free to drop a comment!