-1

I have a coroutine that takes a screenshot and saves to the file system. But when the coroutine runs the game freezes momentarily and the event onFinishedScreenShot fires prematurely.

I thought that Coroutines were "on a background thread".

Is there a way to adjust my code save a screenshot without blocking the main thread and have the event onFinishedScreenShot fire AFTER the write file is finished? Or is there a better way of handling saving screenshots for games?

MySingleton.cs

public event Action<string> onFinishedScreenShot;

public IEnumerator TakeScreenshot(string filename) {

    Texture2D areaImage = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);

    yield return new WaitForEndOfFrame();

    areaImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0, false);
    areaImage.Apply();

    byte[] bytes = areaImage.EncodeToPNG();
    File.WriteAllBytes(filename, bytes);

    if (onFinishedScreenShot != null) {
        onFinishedScreenShot(filename);
    }
}

AnotherClass.cs

private void Start() {
    LoadTexture(someFileName);
}


public void SomeMethodInAnotherClass() {
        StartCoroutine(MySingleton.TakeScreenshot(someFileName));
}


public void LoadTexture(string filename) {

    if (!filename.Equals("NOT SET") ) {

        byte[] bytes = File.ReadAllBytes(GetSaveFilename(filename));

        Texture2D texture = new Texture2D(500, 500, TextureFormat.DXT5, false);
        texture.filterMode = FilterMode.Trilinear;
        texture.LoadImage(bytes);

        RectTransform rt = locationSnapShot.rectTransform;

        Sprite sprite = Sprite.Create(texture, new Rect(0, 0, rt.rect.width, rt.rect.height), new Vector2(0.5f, 0.0f), 16.0f);
        locationSnapShot.sprite = sprite;

        Debug.Log("Removing event listener.");
        MySingleton.onFinishedScreenShot -= LoadTexture;
    } else {
        Debug.Log("No screen shot. Listening for a change.");
        MySingleton.onFinishedScreenShot += LoadTexture;
    }
}
user-44651
  • 3,924
  • 6
  • 41
  • 87

3 Answers3

3

Unity coroutines and "threading" are two entirely different features. Coroutines are a way of managing when your code executes but have nothing to do with threads. Think of the two like this:

Coroutines: "Do X, wait for Y to be done, then do Z."

Threads: "Do X and Y concurrently, then do Z."

If you want to open up your own threads, C# has the ability to do that via System.Threading but you'll need to be careful; almost 100% of Unity APIs (anything coming from the UnityEngine namespace) will assert when they aren't run on the main thread. You'll need to get your raw byte data, open a new thread, then write the file:

...
byte[] bytes = areaImage.EncodeToPNG();

// Start a new thread here
File.WriteAllBytes(filename, bytes);

if (onFinishedScreenShot != null) {
    onFinishedScreenShot(filename);
}

For your last question about the event firing: File.WriteAllBytes is a synchronous call so your event is absolutely being fired after the file is being written (unless you fire that event from anywhere else not listed in the question).

Foggzie
  • 9,691
  • 1
  • 31
  • 48
0

Maybe it's because you have a circular reference in your script.

MySingleton.onFinishedScreenShot += LoadTexture;

You are registering the handler in the handler function. Try doing this in the Start()-function. This could fix the issue.

Doern
  • 7
  • 1
0

This has nothing to do with coroutine. Call the File.WriteAllBytes function in another Thread with ThreadPool.QueueUserWorkItem. First, get UnityThread class from this post which makes it easier to call a function on the main Thread from another Thread. Below is what your code is supposed to look like.

public event Action<string> onFinishedScreenShot;

//Data to pass to the Thread function
public class SaveData
{
    public string fileName;
    public byte[] imgByteArray;
}

void Awake()
{
    //Enable the Thread callback API
    UnityThread.initUnityThread();
}

public IEnumerator TakeScreenshot(string filename)
{

    Texture2D areaImage = new Texture2D(Screen.width, Screen.height, TextureFormat.RGB24, false);

    yield return new WaitForEndOfFrame();

    areaImage.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0, false);
    areaImage.Apply();

    SaveData saveData = new SaveData();
    saveData.fileName = filename;
    saveData.imgByteArray = areaImage.EncodeToPNG();

    //Save on a new Thread
    WriteAllBytes(saveData);
}

void WriteAllBytes(SaveData saveData)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(saveFile));
}

private void saveFile(object state)
{
    SaveData saveData = (SaveData)state;
    File.WriteAllBytes(saveData.fileName, saveData.imgByteArray);

    //Invoke the event on the MainThread
    UnityThread.executeInUpdate(() =>
    {
        if (onFinishedScreenShot != null)
        {
            onFinishedScreenShot(filename);
        }
    });
}
Programmer
  • 121,791
  • 22
  • 236
  • 328
  • I'm currently having the same issue and it actually doesn't come from File IO but instead from the line `areaImage.EncodeToPNG` which might take a while. Unfortunately it uses UnityEngine and as [Foggzie](https://stackoverflow.com/a/47206968/7111561) says there is no way to make this async if Unity doesn't add this – derHugo Nov 08 '18 at 12:31
  • @derHugo There is a way to do that with a native C++ plugin in another Thread. – Programmer Nov 08 '18 at 14:14