7
\$\begingroup\$

Our game includes an in-game level editor. When the user saves a custom level, the level is serialized and written to a file:

public void SaveFile<T>(T obj, string path) { if (!typeof(T).IsSerializable) { throw new ArgumentException("Tried to save non-serializable type " + typeof(T).Name); } FileStream fs = null; try { fs = new FileStream(path, FileMode.Create); DataContractJsonSerializer serializer = GetSerializer(); serializer.WriteObject(fs, obj); } catch (Exception ex) { Debug.LogException(ex); } finally { if (fs != null) fs.Close(); } } 

Serialized files can be loaded like this:

public T LoadFile<T>(string path) { if (!typeof(T).IsSerializable) { throw new ArgumentException("Tried to deserialize non-serializable type " + typeof(T).Name); } if (!File.Exists(path)) return default(T); FileStream fs = null; T result; try { fs = new FileStream(path, FileMode.Open); DataContractJsonSerializer serializer = GetSerializer(); var result = (T)serializer.ReadObject(fs); } catch (Exception ex) { Debug.LogException(ex); throw ex; } finally { if (fs != null) fs.Close(); } return result; } 

When displaying a "Load" menu, we enumerate the saved files like this:

public List<T> LoadFiles() { if (!Directory.Exists(path)) { Debug.Log("No save files found"); return new List<T>(); } var files = Directory.EnumerateFiles(path); Debug.Log("Found " + files.Count() + " files"); List<T> result = new List<T>(files.Count()); foreach (string file in files) { Debug.Log(file); T obj = LoadFile<T>(file); if (obj == null) Debug.Log("File at " + file + " loaded null"); if (obj != null) result.Add(obj); } return result; } 

I have omitted a lot of code for brevity, but what is shown above should cover the important parts.

In the Editor, everything works perfectly. However, in WebGL builds (where files are saved to/loaded from the browser IndexedDB), there is a strange quirk: the file doesn't remain in IndexedDB unless the user changes to a different scene after saving.

Scenario 1:

  1. In the level Editor, Bob presses "Save"
  2. The file is saved, and the browser console shows the log entry File saved to /idbfs/abc123/CustomLevels//FileName
  3. Bob refreshes the webpage
  4. Bob clicks "Load"
  5. The game searches IndexedDB but does not find the file saved in step 2

Scenario 2:

  1. In the level Editor, Alice presses "Save"
  2. The file is saved, and the browser console shows the log entry File saved to /idbfs/abc123/CustomLevels//FileName
  3. Alice returns to the main menu scene
  4. Alice refreshes the webpage
  5. Alice clicks "Load"
  6. The game searches IndexedDB and does find the file saved in step 2

The browser console log suggests that after Bob refreshes the page in Scenario 1, the file no longer exists. If this was the only scenario that Bob had saved, we see the message "No save files found". If Bob had previously saved other scenarios and used the workaround of returning to the main menu scene, those previously saved files will appear in the console and save list, but the most recent file is missing.

Why would the save file disappear from the IndexedDB if the user leaves the webpage without changing scenes, but remain in the IndexedDB if the user returns to the main menu scene before leaving the webpage?

\$\endgroup\$
2
  • 2
    \$\begingroup\$ I notice another user inserting a call to the javascript function syncfs, which looks like it might be used to commit in-memory changes to the persistent DB. It sounds like Unity might be doing something similar on scene loads, that doesn't happen on refresh. You may want to look for a method similar to this that you can call to commit your changes after doing a sensitive operation, before the player has a chance to refresh the page. \$\endgroup\$ Commented Jul 15, 2020 at 22:32
  • \$\begingroup\$ @DMGregory Thanks, that helped me find the answer. \$\endgroup\$ Commented Jul 16, 2020 at 0:32

2 Answers 2

8
\$\begingroup\$

D'oh! This is covered in the WebGL troubleshooting documentation (which did not come up in my web search results for 'Unity IndexedDB').

Unity does not flush changes to IndexedDB immediately when you save a file. They don't explain when they do a flush, but clearly a scene change is one event that triggers a flush.

To immediately flush the changes, we have to run a little bit of JavaScript. Create a new .jslib file in Assets/Plugins/ and add the following code:

mergeInto(LibraryManager.library, { //flush our file changes to IndexedDB SyncDB: function () { FS.syncfs(false, function (err) { if (err) console.log("syncfs error: " + err); }); } }); 

Add a reference to the external JS function in our C# code:

 #if UNITY_WEBGL && !UNITY_EDITOR [DllImport("__Internal")] private static extern void SyncDB(); #endif 

Call the external function after saving:

 #if UNITY_WEBGL && !UNITY_EDITOR //flush our changes to IndexedDB SyncDB(); #endif 
\$\endgroup\$
3
\$\begingroup\$

There's an update on this for newer Unity versions. From issue tracker:

Fixed in 2021.3.44f1, 2022.3.44f1, 6000.0.11f1 (search for "IndexedDB" in release notes)

Fixed an issue that Application.persistentDataPath would not automatically persist, by adding a new JS config option "autoSyncPersistentDataPath: true" to enable automatic synchronization of Application.persistentDataPath over to IndexedDB.

This option is found in the Builds Index.html file, uncomment it and refresh the running build page and the issue will be resolved.

To use this new setting, you can modify the web templates provided by Unity for each installation you want this behavior in. Or better yet, make it team-shared by adding a custom web template in Assets/WebGLTemplates in your project. Whatever you choose, add autoSyncPersistentDataPath: true, into the config that's passed into createUnityInstance() in the template's index.html.


The description for the new config from the docs:

autoSyncPersistentDataPath: true

If set to true, all file writes inside the Unity Application.persistentDataPath directory automatically persist so that the contents are remembered when the user revisits the site the next time. If unset (or set to false), you must manually sync file modifications inside the Application.persistentDataPath directory by calling the JS_FileSystem_Sync() JavaScript function.

It's unset (disabled) by default to not break existing projects. See <Unity Install Location>\Editor\Data\PlaybackEngines\WebGLSupport\BuildTools\WebGLTemplates\Default\index.html for the explanation:

// config.autoSyncPersistentDataPath = true;
This autosyncing is currently not the default behavior to avoid regressing existing user projects that might rely on the earlier manual JS_FileSystem_Sync() behavior, but in future Unity version, this will be expected to change.

\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.