Saving level map file on a different thread to avoid game hiccups

I’m just starting out learning about threads in C#, so I wanted to integrate the most basic example of threading in my current project: Whenever I save a level map file, which may take enough time to make the framerate drop noticeably, I instead want the save method to happen on a new thread, so that the user doesn’t notice any lag while playing and saving.

Might this be worth it or is this a completely contrived example?

Am I doing anything horribly wrong in my following code? Are there any examples of good threaded code in Unity, which you would share or can link to?

public class MapController : MonoBehaviour
{
    Map map;

    void Start()
    {
        var threadStart = new ParameterizedThreadStart(SaveMapThreaded);
        Thread thread = new Thread(threadStart);
        thread.Start(Application.dataPath + "/level.map");
    }

    void SaveMapThreaded(object path)
    {
        var fileManager = new MapFileManager(new JsonMapSerializer());
        fileManager.Save((string)path, map);
    }
}

public class MapFileManager
{
    IMapSerializer serializer;

    // ...

    public void Save(string path, Map map)
    {
        try
        {
            using (FileStream stream = File.Create(path))
            {
                Thread.Sleep(1000);
                serializer.Serialize(stream, MapData.Convert(map));
            }
        }
        catch (Exception ex)
        {
            // If serialization fails, clean up corrupted file.
            if (File.Exists(path))
                File.Delete(path);

            throw ex;
        }
    }
}

public class JsonMapSerializer : IMapSerializer
{
    // ...

    public void Serialize(Stream stream, MapData data)
    {
        var serializer = new JsonSerializer();
        using (var textWriter = new StreamWriter(stream))
        using (var jsonWriter = new JsonTextWriter(textWriter))
        {
            serializer.Serialize(jsonWriter, data);
        }
    }
}

I’ve tested my code with a few Thread.Sleep() calls and it actually does what I want. Even if the save method stalls for a few seconds, the Unity main thread keeps on running smoothly. I know that usually you have to do things like use lock or mutex objects, prevent race conditions and dead locks, but as far as I can think, in my example, there’s not much that could go wrong, right? (I know that most of the Unity API is not threadsafe, but that’s fairly obvious when testing because of the clear exception thrown.)

Thanks for any feedback!

You code looks ok.

There is some Unity functionality that cannot be accessed in a background thread so you need to make sure you don’t do any of that. For instance accessing Application.dataPath is not allowed in a background thread ( at leaast last time i checked )

You have to make sure that you code doesn’t do such things… also you could look into another approach , you could save/load the JSON by making the serializer/deserializer return if it spend too much time on the main thread and resume it by calling an update , that way you can still run it on the main thread. I once had some issues on mobile that I thought I could solve by doing stuff in a background thread but it turned out that a mix of threading and that approach was needed in order to make it really snappy and seamless.

Thanks! Yea, dataPath has to be stored in a variable on the main thread or in my case it is cast to object and passed to the ThreadStart, which works.

I’ve also seen many people prefer coroutines or spread out update loops to perform bigger tasks. Now that I’ve looked into threaded loading of data I can see why. It’s a much bigger hassle to get data back from a thread and making it work with Unity. Most solutions require the main thread to actually poll for changes from other threads, which looks nasty.

Moving data back and forth from threads and main update isn’t that hard, and the polling technically isn’t that nasty.

There’s several ways to do it… but one I found works the smoothest is a coroutine that jumps back and forth.

I implemented this as my ‘RadicalTask’ in my framework:

But someone else implemented it in a more distributable manner that can be easily integrated into your project (unlike my framework) and is free on the asset store:

With that, it’s SUPER easy to jump between thread and coroutine.

1 Like