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

时间:2018-10-29 21:38:30

标签: c# unity3d stream coroutine

在更新进度条时,我试图逐行读取文件(两个GUI纹理,其宽度之一(最大宽度* 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: https://stackoverflow.com/questions/41296957/wait-while-file-load-in-unity
    // Thanks to: https://stackoverflow.com/a/34378847/3286975
    [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);
  }

我共享的最后一个代码包含两个部分:

  • 被注释的部分也会阻塞主线程。
  • 我使用的WWW类(以后将不推荐使用)不会阻塞主线程,但是它仅在进度条上显示两个步骤(例如25%和70%)。

我不知道为什么会这样,如果有更好的方法可以解决这个问题。

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

1 个答案:

答案 0 :(得分:1)

  

被注释的部分也会阻塞主线程。

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

问题是某些人不了解www.isDone的用法和使用时间。一个示例是this帖子,当人们找到这样的代码时,就会遇到问题。

使用UnityWebRequest的两种方法:

1 。下载或提出请求,然后将其忘记,直到完成。当您不需要需要了解下载状态时,例如UnityWebRequest.downloadedBytesUnityWebRequest.uploadedBytesUnityWebRequest.downloadProgressUnityWebRequest.uploadProgress

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

示例(请注意使用yield return www.SendWebRequest() 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.downloadProgressUnityWebRequest.uploadProgress,您可以执行此操作,并且可以在等待UnityWebRequest.isDone成为true循环时进行检查。

在这种情况下,您无法等待或产生UnityWebRequest.SendWebRequest()函数,因为产生该函数会一直等待直到请求完成,从而无法检查下载状态。只需像普通函数一样调用UnityWebRequest.SendWebRequest()函数,然后使用whileUnityWebRequest.isDone循环中进行等待。

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

示例(请注意使用UnityWebRequest.isDone 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)一起使用之前检查了错误。您必须这样做。


  

我使用的WWW类(以后将不推荐使用)不会被阻止   主线程,但它仅在进度条上显示两个步骤   (例如25%和70%)。

如果的确如此,那么很可能在解决了我与UnityWebRequest讨论过的问题之后,UnityWebRequest API也可能会像WWW API一样为您提供25%和70%的收益。

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

获取UnityThread脚本,该脚本用于回调主线程。这样做是为了可以将回调返回的值与主线程上的Unity API一起使用。

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);
});