在Unity中使用协程更新进度条时读取文件

Read file while updating progress bar with coroutines in Unity

我试图在更新进度条时逐行读取文件(两个 GUI 纹理,其中一个宽度 (maxWidth * currentPercentage) 有浮动扩展)。

我有两个实现:

    public static string ThreadedFileRead(string path, Action<float> percAction)
    {
        FileInfo fileInfo = new FileInfo(path);
        StringBuilder sb = new StringBuilder();

        float length = fileInfo.Length;
        int currentLength = 0;

        using (StreamReader sr = new StreamReader(path))
        {
            while (!sr.EndOfStream)
            {
                string str = sr.ReadLine();
                sb.AppendLine(str);

                // yield return str;

                percAction(currentLength / length);
                currentLength += str.Length;
                Interlocked.Add(ref currentLength, str.Length);
            }

            percAction(1f);

            return sb.ToString();
        }
    }

使用以下实现:

  // Inside a MonoBehaviour

  public void Start() 
  {
       string fileContents = "";
       StartCoroutine(LoadFileAsync(Application.dataPath + "/Data/file.txt", (s) => fileContents = s));
  }

  public IEnumerator LoadFileAsync(string path, Action<string> fin) 
  {
        string contents = "";

        lock (contents)
        {
            var task = Task.Factory.StartNew(() =>
            {
                contents = F.ThreadedFileRead(path, (f) => currentLoadProgress = f);
            });

            while (!task.IsCompleted)
                yield return new WaitForEndOfFrame();

            fin?.Invoke(contents);
        }
  }

但这会阻塞当前的 GUI(我不知道为什么)。

我也用过这个:

    // Thanks to: 
    // Thanks to: 
    [MustBeReviewed]
    public static IEnumerator LoadFileAsync(string pathOrUrl, Action<float> updatePerc, Action<string> finishedReading)
    {
        FileInfo fileInfo = new FileInfo(pathOrUrl);

        float length = fileInfo.Length;

        // Application.isEditor && ??? // Must review
        if (Path.IsPathRooted(pathOrUrl))
            pathOrUrl = "file:///" + pathOrUrl;

        /*

        using (var www = new UnityWebRequest(pathOrUrl))
        {
            www.downloadHandler = new DownloadHandlerBuffer();

            CityBenchmarkData.StartBenchmark(CityBenchmark.SendWebRequest);

            yield return www.SendWebRequest();

            CityBenchmarkData.StopBenchmark(CityBenchmark.SendWebRequest);

            while (!www.isDone)
            {
                // www.downloadProgress
                updatePerc?.Invoke(www.downloadedBytes / length); // currentLength / length
                yield return new WaitForEndOfFrame();
            }

            finishedReading?.Invoke(www.downloadHandler.text);
        }

         */

        using (var www = new WWW(pathOrUrl))
        {
            while (!www.isDone)
            {
                // www.downloadProgress
                updatePerc?.Invoke(www.bytesDownloaded / length); // currentLength / length
                yield return new WaitForEndOfFrame();
            }

            finishedReading?.Invoke(www.text);
        }
    }

实施如下:

  public IEnumerator LoadFileAsync(string path, Action<string> fin) 
  {
       yield return F.LoadFileAsync(path, (f) => currentLoadProgress = f, fin);
  }

我分享的最后一个代码有两部分:

我不知道为什么会这样,也不知道是否有更好的方法。

因此,欢迎为此提供任何帮助(指导)。

The commented part blocks also the main thread.

不是"blocking"主线程。如果它阻塞了主线程,编辑器也会冻结,直到加载或下载完成。它只是在等待 www.SendWebRequest() 完成,在等待的同时,您项目中的其他脚本仍在正常 运行 每一帧运行。

问题是有些人不明白www.isDone是怎么用的,什么时候用。一个例子是 this post,当人们找到这样的代码时,他们 运行 会提出问题。

两种使用方式UnityWebRequest:

1。下载或提出请求,然后忘记它,直到完成。当您 不需要 需要知道下载状态时,例如 UnityWebRequest.downloadedBytesUnityWebRequest.uploadedBytesUnityWebRequest.downloadProgress 和 [=20],您可以执行此操作=] 值。

为此,您需要使用 yield return 关键字生成 UnityWebRequest.SendWebRequest() 函数。它将等待 UnityWebRequest.SendWebRequest() 函数,直到下载完成,但它不会阻止您的程序。 Update 函数和其他脚本中的代码应该仍然是 运行ning。该等待仅在您的 LoadFileAsync 协程函数中完成。

示例(注意使用 yield return www.SendWebRequest()no UnityWebRequest.isDone:

using (var www = new UnityWebRequest(pathOrUrl))
{
    www.downloadHandler = new DownloadHandlerBuffer();
    yield return www.SendWebRequest();

    if (www.isHttpError || www.isNetworkError)
    {
        Debug.Log("Error while downloading data: " + www.error);
    }
    else
    {
        finishedReading(www.downloadHandler.text);
    }
}

2。下载或发出请求,然后等待每一帧,直到 UnityWebRequest.isDonetrue。当您 需要 了解下载状态时,您可以执行此操作,例如 UnityWebRequest.downloadedBytesUnityWebRequest.uploadedBytesUnityWebRequest.downloadProgress 以及 UnityWebRequest.uploadProgress 值这些可以在等待 UnityWebRequest.isDone 成为循环中的 true 时进行检查。

在这种情况下,您 不能 等待或放弃 UnityWebRequest.SendWebRequest() 函数,因为放弃它会一直等到请求完成,从而无法检查下载状态。只需像普通函数一样调用 UnityWebRequest.SendWebRequest() 函数,然后使用 UnityWebRequest.isDone.

while 循环中等待

等待帧是在 yield return nullyield return new WaitForEndOfFrame() 循环中完成的,但建议使用 yield return null,因为这不会创建 GC。

示例(注意使用 UnityWebRequest.isDoneno yield return www.SendWebRequest():

using (var www = new UnityWebRequest(pathOrUrl))
{
    www.downloadHandler = new DownloadHandlerBuffer();
    //Do NOT yield the SendWebRequest function when using www.isDone
    www.SendWebRequest();

    while (!www.isDone)
    {
        updatePerc.Invoke(www.downloadedBytes / length); // currentLength / length
        yield return null;
    }

    if (www.isHttpError || www.isNetworkError)
    {
        Debug.Log("Error while downloading data: " + www.error);
    }
    else
    {
        finishedReading(www.downloadHandler.text);
    }
}

您正在混合 #1#2。在您的情况下,您需要使用 #2。请注意,在这两种情况下,我在下载之后和使用 if (www.isHttpError || www.isNetworkError) 使用加载的数据之前检查了错误。你必须这样做。


The WWW class I used (it will be deprecated in a future) doesn't block the main thread, but it only displays two steps on the progress bar (like 25% and 70%).

如果那是真的,那么很可能在解决我与 UnityWebRequest 谈到的问题后,UnityWebRequest API 可能会给你 25% 和 70% 的赞WWW API 也是。

您看到 25%70% 的原因是因为文件非常小,所以 Unity API 加载速度很快,跳过了一些百分比值。这对于 Unity 的 API 是正常的。只需使用任何 C# System.IO API 读取文件即可解决此问题。使您的 ThreadedFileRead 函数 return 通过 Action 的结果就像您的 LoadFileAsync 函数一样。这将使线程的实现变得更容易。

获取用于回调主线程的UnityThread脚本。您这样做是为了可以在主线程上使用通过 Unity API 从回调中编辑的值 return。

Awake函数中初始化线程回调脚本,然后使用ThreadPool.QueueUserWorkItemTask.Factory.StartNew处理文件加载。要将值发送回主线程,请使用 UnityThread.executeInUpdate 进行调用。

void Awake()
{
    UnityThread.initUnityThread();
}

public static void ThreadedFileRead(string path, Action<float> percAction, Action<string> finishedReading)
{
    /* Replace Task.Factory with ThreadPool when using .NET <= 3.5
     * 
     * ThreadPool.QueueUserWorkItem(state =>
     * 
     * */

    var task = Task.Factory.StartNew(() =>
    {
        FileInfo fileInfo = new FileInfo(path);
        StringBuilder sb = new StringBuilder();

        float length = fileInfo.Length;
        int currentLength = 0;

        using (StreamReader sr = new StreamReader(path))
        {
            while (!sr.EndOfStream)
            {
                string str = sr.ReadLine();
                sb.AppendLine(str);

                // yield return str;

                //Call on main Thread
                UnityThread.executeInUpdate(() =>
                 {
                     percAction(currentLength / length);
                 });

                currentLength += str.Length;
                //Interlocked.Add(ref currentLength, str.Length);
            }

            //Call on main Thread
            UnityThread.executeInUpdate(() =>
             {
                 finishedReading(sb.ToString());
             });
        }
    });
}

用法:

ThreadedFileRead(path, (percent) =>
{
    Debug.Log("Update: " + percent);
}, (result) =>
{
    Debug.Log("Done: " + result);
});