6
\$\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\$
    – DMGregory
    Jul 15, 2020 at 22:32
  • \$\begingroup\$ @DMGregory Thanks, that helped me find the answer. \$\endgroup\$
    – Kevin
    Jul 16, 2020 at 0:32

1 Answer 1

6
\$\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\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .