Saving game state with HTML5 localStorage and JSON - a rough working draft

I am typing this while bouncing on the couch…happily.

I have just written the messiest localStorage setup that anyone could probably ever write. Ever. This thing is cringeworthy. But I’m happy. Because it works. And it works like this.

My first functional localStorage draft

Storing data to localStorage

In the loadLevel() function, every time LevelFinish loads (which is the level that comes after every successful level completion), this happens:

                if (!this.gameRestored) {
                    ig.game.Controller.saveGame();
                }

The saveGame() function is in the Controller class and saves all of the attributes that are relevant to the current game and the player’s progress within it:

    saveGame: function() {
        localStorage.setItem("nextLevel", ig.game.nextLevel);
        localStorage.setItem("playerKind", this.playerKind);
        localStorage.setItem("accelGround", this.accelGround);
        localStorage.setItem("accelAir", this.accelAir);
        localStorage.setItem("coreDamage", this.coreDamage);
        localStorage.setItem("speedModifier", this.speedModifier);
        localStorage.setItem("fuel", this.fuel);
        localStorage.setItem("turnSpeed", this.turnSpeed);
        localStorage.setItem("thrust", this.thrust);
        localStorage.setItem("playerHealth", this.playerHealth);
        localStorage.setItem("levelKills", this.levelKills);
        localStorage.setItem("totalKills", this.totalKills);
        localStorage.setItem("levelArtifacts", this.levelArtifacts);
        localStorage.setItem("totalArtifacts", this.totalArtifacts);
        localStorage.setItem("levelScore", this.levelScore);
        localStorage.setItem("totalScore", this.totalScore);
        localStorage.setItem("fuelLeft", this.fuelLeft);
        localStorage.setItem("populationLeft", this.populationLeft);
        localStorage.setItem("foodLeft", this.foodLeft);
        localStorage.setItem("waterLeft", this.waterLeft);
        localStorage.setItem("currentLocation", this.currentLocation);
        localStorage.setItem("allLevels", JSON.stringify(ig.game.allLevels));
        localStorage.setItem("weaponsArr", JSON.stringify(ig.game.inventory.weaponsArr));
        localStorage.setItem("newArtifactsArr", JSON.stringify(this.extractName(ig.game.inventory.newArtifactsArr)));
        localStorage.setItem("allArtifactsArr", JSON.stringify(this.extractName(ig.game.inventory.allArtifactsArr)));
        localStorage.setItem("allUpgradesArr", JSON.stringify(this.extractName(ig.game.inventory.allUpgradesArr)));
    },

localStorage seems to only be able to handle strings, so arrays need to be turned into strings with JSON.stringify. However, some of my arrays contain circular references (namely newArtifactsArr, allArtifactsArr, and allUpgradesArr), which throws an exception when trying to store.

To get around this problem instead of storing the entire array of objects I decided to store only the name of each object in the array and then use these names to get the items associated with them down the line. So for those three arrays I use the extractName() function:

    extractName: function(object) {
        var newArray = new Array();
        for (var i = 0; i < object.length; i++) {
            newArray.push(object[i].name);
            console.log('added ' + object[i].name);
        }
        return newArray;
    },

Getting stored data from localStorage

That’s it for the saving. Now, every time the game starts the following runs in the main.js init():

        if (localStorage.getItem("playerKind") === null) {
            this.loadLevel(LevelTitle);
        }

        else {
            ig.game.Controller.restoreGame();
            this.gameRestored = true;
        }

restoreGame() looks like this:

    restoreGame: function() {
        ig.game.nextLevel = localStorage.getItem("nextLevel");
        this.playerKind = localStorage.getItem("playerKind");
        this.accelGround = localStorage.getItem("accelGround");
        this.accelAir = localStorage.getItem("accelAir");
        this.coreDamage = localStorage.getItem("coreDamage");
        this.speedModifier = localStorage.getItem("speedModifier");
        this.fuel = localStorage.getItem("fuel");
        this.turnSpeed = localStorage.getItem("turnSpeed");
        this.thrust = localStorage.getItem("thrust");
        this.playerHealth = localStorage.getItem("playerHealth");
        this.levelKills = localStorage.getItem("levelKills");
        this.totalKills = localStorage.getItem("totalKills");
        this.levelArtifacts = localStorage.getItem("levelArtifacts");
        this.totalArtifacts = localStorage.getItem("totalArtifacts");
        this.levelScore = localStorage.getItem("levelScore");
        this.totalScore = localStorage.getItem("totalScore");
        this.fuelLeft = localStorage.getItem("fuelLeft");
        this.populationLeft = localStorage.getItem("populationLeft");
        this.foodLeft = localStorage.getItem("foodLeft");
        this.waterLeft = localStorage.getItem("waterLeft");
        this.currentLocation = localStorage.getItem("currentLocation");
        ig.game.inventory.weaponsArr = JSON.parse(localStorage.getItem("weaponsArr"));
        ig.game.inventory.allLevels = JSON.parse(localStorage.getItem("allLevels"));
        var newArtifactsArr = JSON.parse(localStorage.getItem("newArtifactsArr"));
        var allArtifactsArr = JSON.parse(localStorage.getItem("allArtifactsArr"));
        var allUpgradesArr = JSON.parse(localStorage.getItem("allUpgradesArr"));

        for (var i = 0; i < newArtifactsArr.length; i++ ) {
            this.repopulateArray(newArtifactsArr[i], ig.game.inventory.newArtifactsArr);
        }

        for (var i = 0; i < allArtifactsArr.length; i++ ) {
            this.repopulateArray(allArtifactsArr[i], ig.game.inventory.allArtifactsArr);
        }

        for (var i = 0; i < allUpgradesArr.length; i++ ) {
            this.repopulateArray(allUpgradesArr[i], ig.game.inventory.allUpgradesArr);
        }

        ig.game.loadLevel(LevelFinish);
    },

The arrays with the circular references I mentioned before are then repopulated with the following function by matching each element inside the array (the item’s name) with the item object itself (that holds the rest of that item’s attributes):

    repopulateArray: function(name, array) {
        for (var i = 0; i < ig.game.inventory.allItems.length; i++) {
            if (ig.game.inventory.allItems[i].name == name) {
                array.push(ig.game.inventory.allItems[i]);
                break;
            }
        }
    },

At the end of the loadLevel() function in main.js, gameRestored is set back to false to make sure the game saves again after the next level:

this.gameRestored = false;

Testing

Like I said, this is super messy and isn’t even properly tested yet because I only have one level to test with so far. I know it works with this one level, but it’s possible that I’ve overlooked some glaring mistake that will make itself known later down the track.

So far I’ve tested reopening the game in a new tab, after a browser restart, after a MAMP server restart, and after a PC restart. Seems to work fine with these rough initial tests, but there is a long way to go before I can be confident that this is working the way I need it to be.

Revisions to make

I know there’s a way to loop through localStorage and get items that way, which I am eventually planning on looking into. For now I want to continue to get each item individually at least until I thoroughly test this thing. I am sure there are plenty of bugs here that will need to be fixed.

For now, I’m just happy that I have some sort of working version of this. Game state saving was the thing I was dreading the most, thinking it was going to be a hugely complicated ordeal. I know my current solution is far from perfect, but it was a lot less of a pain than I thought it would be to get something up and running.

comments powered by Disqus