Saving game data in WEBGL when using Unity

Intro
I have a game called Mathemando on itch.io. There player has to find the arithmetical formula to match the result.
After I got the initial feedback, I decided to reimplement the difficulty, allow player to set it up. I implemented it (more on it in the next article), built the app, pushed to itch only to find that all the progress I had before got reset.
Well. That’s a bummer. I had like 400 completed levels there. I am the dev and I can cheat but not this time. I wanna eat my own dog food :D
I gave it another test, played some levels and re-uploaded absolutely the same build. Result? The same. Nothing is saved.
The cause and solution
In Unity, if you use the default path Application.persistentDataPath
and upload to itch, then whatever you save in the persistentDataPath
will remain there only till you upload the new build. Afterwards that all is gone because the persistent data path changes with each build upload. it is related to how build is processed after uploading.
To fix that issue you have to create your own, truly persistent path. A very nice post on the topic: https://ddmeow.net/en/game-dev/save-persistent-itch-io/. Long story short, you have to make your own path to save the file properly in the indexed database and then save each file separately. The whole logic I have in storage is below:
public static class PersistanceStorage
{
private static string mPersistentDataPath;
static PersistanceStorage()
{
#if UNITY_WEBGL
mPersistentDataPath = "idbfs/Mathemando-Little-Cool-Puzzle-3141"; // just to have as random path as possible. Not needed
Debug.Log($"[PrefsStorage] Using WebGL persistent path: {mPersistentDataPath}");
#else
mPersistentDataPath = Application.persistentDataPath;
Debug.Log($"[PrefsStorage] Using default persistent path: {mPersistentDataPath}");
#endif
if (!Directory.Exists(mPersistentDataPath))
{
Debug.Log($"[PrefsStorage] Directory does not exist. Creating directory: {mPersistentDataPath}");
Directory.CreateDirectory(mPersistentDataPath);
}
else
{
Debug.Log($"[PrefsStorage] Directory already exists: {mPersistentDataPath}");
}
}
public static void SaveToFile<T>(string key, T value)
{
string filePath = GetFilePath(key);
string json = JsonUtility.ToJson(new Wrapper<T> { Value = value });
try
{
File.WriteAllText(filePath, json);
catch (IOException e)
{
Debug.LogError($"[PrefsStorage] Failed to save key '{key}' to file: {filePath}. Exception: {e.Message}");
}
}
public static T LoadFromFile<T>(string key, T defaultValue)
{
string filePath = GetFilePath(key);
if (File.Exists(filePath))
{
try
{
string json = File.ReadAllText(filePath);
T value = JsonUtility.FromJson<Wrapper<T>>(json).Value;
return value;
}
catch (IOException e)
{
Debug.LogError($"[PrefsStorage] Failed to load key '{key}' from file: {filePath}. Exception: {e.Message}");
}
}
else
{
Debug.LogWarning($"[PrefsStorage] File for key '{key}' does not exist. Returning default value: {defaultValue}");
}
return defaultValue;
}
private static string GetFilePath(string key)
{
string filePath = Path.Combine(mPersistentDataPath, $"{key}.json");
return filePath;
}
[System.Serializable]
private class Wrapper<T>
{
public T Value;
}
As PlayerPrefs
have the same issue, I stopped using them completely. It's a shame because that is really convenient. I uploaded the build, gave a test run and.. saw the same behaviour! Or rather so: the data was saved only once in a while, completely random. Sometimes after 5 seconds, sometimes not even after a minute after the update. So upon the browser refresh I would land on the same old save data I had despite seeing that all the methods are called.
The second issue and solution
File system in browser seems to be lazier and does not flush the data immediately. So we have to force it! Again, internet to the rescue, thanks to this answer: https://discussions.unity.com/t/system-io-file-doesnt-work-properly-on-webgl-platform/905164/3
All we need to do is to synchronise the data.
public static void SaveToFile<T>(string key, T value)
{
string filePath = GetFilePath(key);
string json = JsonUtility.ToJson(new Wrapper<T> { Value = value });
try
{
File.WriteAllText(filePath, json);
#if UNITY_WEBGL
// Synchronize the filesystem to ensure changes are persisted
Application.ExternalEval("_JS_FileSystem_Sync();");
#endif
}
catch (IOException e)
{
Debug.LogError($"[PrefsStorage] Failed to save key '{key}' to file: {filePath}. Exception: {e.Message}");
}
}
And now it works! Or at least I have not experienced any issues. Doing stuff in web is totally unpredictable.
Learnings
If you have persistence in your project, be it in web or on mobile, have a second "shadow" project and test your releases there first before touching the main release. Because if you have a lot of players they will have.. a lot of disappointment! That was not my case though :D at least, I hope I did not discourage too much those couple of people who visit my game.
PS
Perhaps, I have just missed some point though. I know that it's often the user who's guilty of the bug :D
A personal observation. I like asking chatbots to write solutions. The problem is that as soon as the issue is not massively spammed in the internet it just writes gibberish. So programmers can remain more than ai-handling project managers.. at least for now.
Subscribe to my newsletter
Read articles from Pavel Stepanov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Pavel Stepanov
Pavel Stepanov
I am an iOS developer developing the best-rated banking app in Germany from before it went beta 🇩🇪 I focus on all the aspects of software engineering, from writing good code to developing scalable architectures to enhancing UX and other not-so-much-code-related things. I also put a lot of attention to establishing communication flows and creating a great atmosphere. One of my other focuses is providing a constant flow of cats and memes all over the place. I think in this part I succeeded the most.