Data Persistence

Saving Games Online in Unity WebGL

Posted by Aaron Salisbury on April 2, 2016

Clicking games are, in fact, barely games. That considered, a few of us got caught up in Canada Clicker and enjoyed how ridiculous it was. During this vacuous time, I was challenged to make a trite clicker of my own (Night of the Clicking Dead). I thought it would be a good opportunity to try some things that I haven't before. As it turns out, I decided to make this my first WebGL project that would allow the user to save their game state. After all, it wouldn't be a clicker if you couldn't gain advancements that automatically accrued between sessions. I'm going to discuss the save process I implemented as well as my leveraging of JavaScript alerts used in error handling. Done so entirely within Unity, opposed to having to add the script to the webpage directly.

Serializable Game Details

I created a standard class to house all of the player's session details. This class is instantiated and used in my Unity game manager class to keep track of everything.


using System;

[Serializable]
public class GameDetails
{
    public decimal XP;
    public decimal XPMultiplier;
    public decimal NextXPLevelUp;
    public int DoubleXPCost;
    public int CurrentLevel;
    public int ClickCount;
    public int BrainCount;
    public int CurrentGun;
    public int AutomationLevel;
    public DateTime? SaveDate;
    public bool ProgressBarIsFull;
    
    public GameDetails()
    {
        XP = 0.0M;
        XPMultiplier = 1.0M;
        NextXPLevelUp = 100.0M;
        DoubleXPCost = 2000;
        CurrentLevel = 1;
        ClickCount = 0;
        BrainCount = 0;
        CurrentGun = 0;
        AutomationLevel = 0;
        SaveDate = null;
        ProgressBarIsFull = false;
    }
    
    public void IncrementClickCount()
    {
        ...
    }

    public void LevelUp()
    {
        ...
    }

    public void AutomateClicksBetweenSessions()
    {
        ...
    }
}

The class must me marked as serializable and the properties themselves must also be serializable.

The Data Access Layer

For this project, the entirety of the data access logic goes into one class. Here we save and load games.


public static void Save(GameDetails gameDetails)
{
    string dataPath = string.Format("{0}/GameDetails.dat", Application.persistentDataPath);
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    FileStream fileStream;

    try
    {
        if (File.Exists(dataPath))
        {
            File.WriteAllText(dataPath, string.Empty);
            fileStream = File.Open(dataPath, FileMode.Open);
        }
        else
        {
            fileStream = File.Create(dataPath);
        }

        binaryFormatter.Serialize(fileStream, gameDetails);
        fileStream.Close();

        if (Application.platform == RuntimePlatform.WebGLPlayer)
        {
            SyncFiles();
        }
    }
    catch (Exception e)
    {
        PlatformSafeMessage("Failed to Save: " + e.Message);
    }
}

public static GameDetails Load()
{
    GameDetails gameDetails = null;
    string dataPath = string.Format("{0}/GameDetails.dat", Application.persistentDataPath);
        
    try
    {
        if (File.Exists(dataPath))
        {
            BinaryFormatter binaryFormatter = new BinaryFormatter();
            FileStream fileStream = File.Open(dataPath, FileMode.Open);

            gameDetails = (GameDetails)binaryFormatter.Deserialize(fileStream);
            fileStream.Close();
        }
    }
    catch (Exception e)
    {
        PlatformSafeMessage("Failed to Load: " + e.Message);
    }

    return gameDetails;
}

private static void PlatformSafeMessage(string message)
{
    if (Application.platform == RuntimePlatform.WebGLPlayer)
    {
        WindowAlert(message);
    }
    else
    {
        Debug.Log(message);
    }
}

The first key point here is the use of Application.persistentDataPath. Unity already went through the trouble of making this platform safe. This is where our save file will be physically stored.

The second area of note is that when saving in WebGL, we need to call SyncFiles(). This lets us leverage IDBFS, which provides a POSIX-like file system interface for browser-based JavaScript.

Non-Compiling Unity JavaScript

In order for PlatformSafeMessage() to work and to be able to call SyncFiles(), we have to interact with browser JavaScript. The standard way to do this is to use the Application.ExternalCall() and Application.ExternalEval() functions and to manually embed the script on your webpage. That is gross. Fortunately we can now add our JavaScript sources to the project, and then call those functions directly from script code. To do so, place files with JavaScript code using the .jslib extension (as the normal .js would be picked up by the UnityScript compiler) into a "Plugins/WebGL" folder in your Assets folder. The file needs to have a syntax like this:

Assets/Plugins/WebGL/HandleIO.jslib


var HandleIO = {
    WindowAlert : function(message)
    {
        window.alert(Pointer_stringify(message));
    },
    SyncFiles : function()
    {
        FS.syncfs(false,function (err) {
            // handle callback
        });
    }
};

mergeInto(LibraryManager.library, HandleIO);

In order to use a string parameter from our scripts, we have to send it through Pointer_stringify().

Accessing Our JavaScript

Back in our data access class we must import the external methods. Here is the complete class.


using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;

public class DataAccess
{
    [DllImport("__Internal")]
    private static extern void SyncFiles();

    [DllImport("__Internal")]
    private static extern void WindowAlert(string message);

    public static void Save(GameDetails gameDetails)
    {
        string dataPath = string.Format("{0}/GameDetails.dat", Application.persistentDataPath);
        BinaryFormatter binaryFormatter = new BinaryFormatter();
        FileStream fileStream;

        try
        {
            if (File.Exists(dataPath))
            {
                File.WriteAllText(dataPath, string.Empty);
                fileStream = File.Open(dataPath, FileMode.Open);
            }
            else
            {
                fileStream = File.Create(dataPath);
            }

            binaryFormatter.Serialize(fileStream, gameDetails);
            fileStream.Close();

            if (Application.platform == RuntimePlatform.WebGLPlayer)
            {
                SyncFiles();
            }
        }
        catch (Exception e)
        {
            PlatformSafeMessage("Failed to Save: " + e.Message);
        }
    }

    public static GameDetails Load()
    {
        GameDetails gameDetails = null;
        string dataPath = string.Format("{0}/GameDetails.dat", Application.persistentDataPath);
        
        try
        {
            if (File.Exists(dataPath))
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                FileStream fileStream = File.Open(dataPath, FileMode.Open);

                gameDetails = (GameDetails)binaryFormatter.Deserialize(fileStream);
                fileStream.Close();
            }
        }
        catch (Exception e)
        {
            PlatformSafeMessage("Failed to Load: " + e.Message);
        }

        return gameDetails;
    }

    private static void PlatformSafeMessage(string message)
    {
        if (Application.platform == RuntimePlatform.WebGLPlayer)
        {
            WindowAlert(message);
        }
        else
        {
            Debug.Log(message);
        }
    }
}

That's it. Now you're serializing your save data to the browser. Users can play your online games and come and go without having to start over. A basic but important concept, funny I got to it with a clicker.

Learn more about basic saving in Unity.

Learn more about browser/script interaction.