在 Unity3D 中,如何在迭代 FileInfo[] 数组(非阻塞)时异步显示 UI 径向进度条?

Posted

技术标签:

【中文标题】在 Unity3D 中,如何在迭代 FileInfo[] 数组(非阻塞)时异步显示 UI 径向进度条?【英文标题】:In Unity3D, how can I asynchronously display a UI radial progress bar while iterating a FileInfo[] array (non-blocking)? 【发布时间】:2020-11-02 03:47:11 【问题描述】:

当我运行游戏时,GetTextures 方法的一部分会花费一些时间,并且游戏会冻结,直到它加载所有图像。我希望它在径向进度条中显示循环进度,可能在不同的线程上使用协程来不阻塞主线程。

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Video;

public class StreamVideo : MonoBehaviour

    public Texture2D[] frames;                // array of textures
    public float framesPerSecond = 2.0f;    // delay between frames
    public RawImage image;
    public int currentFrameIndex;

    public GameObject LoadingText;
    public Text ProgressIndicator;
    public Image LoadingBar;
    float currentValue;
    public float speed;

    void Start()
    
        DirectoryInfo dir = new DirectoryInfo(@"C:\tmp");
        // since you use ToLower() the capitalized version are quite redundant btw ;)
        string[] extensions = new[]  ".jpg", ".jpeg", ".png" ;
        FileInfo[] info = dir.GetFiles().Where(f => extensions.Contains(f.Extension.ToLower())).ToArray();

        if (!image)
        
            //Get Raw Image Reference
            image = gameObject.GetComponent<RawImage>();
        

        frames = GetTextures(info);
        foreach (var frame in frames)
            frame.Apply(true, true);

    

    private Texture2D[] GetTextures(FileInfo[] fileInfos)
    
        var output = new Texture2D[fileInfos.Length];
        for (var i = 0; i < fileInfos.Length; i++)
        
            var bytes = File.ReadAllBytes(fileInfos[i].FullName);
            output[i] = new Texture2D(1, 1);
            if (!ImageConversion.LoadImage((Texture2D)output[i], bytes, false))
            
                Debug.LogError($"Could not load image from fileInfos.Length!", this);
            
        

        return output;
    

    void Update()
    
        int index = (int)(Time.time * framesPerSecond) % frames.Length;
        image.texture = frames[index]; //Change The Image

        if (currentValue < 100)
        
            currentValue += speed * Time.deltaTime;
            ProgressIndicator.text = ((int)currentValue).ToString() + "%";
            LoadingText.SetActive(true);
        
        else
        
            LoadingText.SetActive(false);
            ProgressIndicator.text = "Done";
        

        LoadingBar.fillAmount = currentValue / 100;
    

    private void OnDestroy()
    
        foreach (var frame in frames)
            Destroy(frame);
    

现在只是为了测试,我在更新中为径向进度条添加了一些代码:

if (currentValue < 100)
        
            currentValue += speed * Time.deltaTime;
            ProgressIndicator.text = ((int)currentValue).ToString() + "%";
            LoadingText.SetActive(true);
        
        else
        
            LoadingText.SetActive(false);
            ProgressIndicator.text = "Done";
        

        LoadingBar.fillAmount = currentValue / 100;

但它也只有在 GetTextures 方法完成它的操作后才会启动。 主要目标是在径向进度条中的 GetTextures 中显示操作的进度。

【问题讨论】:

您可以使用新的 Unity UI 并将 Image Sprites 用于前景和背景,并将其填充为红色或绿色。我对玩家和敌人的生命条做了类似的事情。基本上它一开始都是绿色的,当玩家或敌人受到攻击时,填充值从 1.0 下降到 0.0,增量为 0,0x。您可以将文本和倍数叠加 100(以形成百分比),我会看看是否能找到我的 UI 图像代码。 我确实记得使用委托和静态事件。我创建了一个 HealthSystem 类,Update() 将触发 onHealthChange 委托,静态事件将被称为递增或递减生命整数值。 AddHealth(int x) 或 SubtractHealth(int x) 也找到了这个 -> xinyustudio.wordpress.com/2015/08/06/… @ApolloSOFTWARE 我想说问题主要不是如何使用不同的 UI 资产构建进度条,而是如何为给定的文件 IO 和加载纹理过程生成进度;)跨度> 你可以,但你不应该...... WWW 已过时并被UnityWebRequest 取代......但是如何加载音频文件类似于从硬盘加载各种图像文件? ;) 【参考方案1】:

前段时间,我使用 WWW 和字节数组为 Unity3D 编写了一个音乐下载器和播放器,它在不阻塞主线程的情况下运行并使用 FileInfo。

该类创建一个 WWW 请求以从公共字符串 [] 歌曲下载 mp3,当 WWW 类型 (request.isDone) 时,它将 File.WriteAllBytes 到您的本地游戏/应用程序路径(唉,播放 AudioClip) .我想知道您是否可以修改此代码,以便您可以使用您的 c:\tmp\ 并以某种方式获取 *.jpeg、*.jpg、*.png 并根据需要保留它并更新您的进度而不会阻塞。您必须在路径字符串中使用 file://c:/tmp:

request = new WWW(path);

下面代替 jpg、jpeg 和 pngs 的代码使用 File.WriteAllBytes 将 .mp3(.au 或 .wav 可以工作)保存到 Application.persistentDataPath 并最终作为音频剪辑播放。由于它使用协程,因此没有主线程阻塞,因此用户不会冻结。您当然需要修改此代码以将您的图像保存为 Sprites 或 Raw Image 类型。我不确定它是否能满足您的需求或可以工作,但如果您可以使用任何此代码并使用它来不以任何身份阻塞主线程,我会很高兴。

下面的代码已经有 5 年的历史了,如果某些类可能被弃用,我们深表歉意。

using UnityEngine;
using System.Collections;
using System.Net;
using System.IO;

public class PlayMusic : MonoBehaviour 
    public string[] songs;
    public string currentSong;
    public float Size = 0.0f;
    public int playMusic;
    private int xRand;
    private string temp;
    WWW request;
    byte[] fileData;

    IEnumerator loadAndPlayAudioClip(string song) 
        string path = song;
        currentSong = song.Substring(song.LastIndexOf('/')+1);
        temp = currentSong;
    
        FileInfo info = new FileInfo(Application.persistentDataPath + "/" + song.Substring(song.LastIndexOf('/')+1));
        if (info.Exists == true) 
            print("file://"+Application.persistentDataPath + "/" + song.Substring(song.LastIndexOf('/')+1) + " exists");
            path = "file:///" + Application.persistentDataPath + "/" + song.Substring(song.LastIndexOf('/')+1);
        

        // Start a download of the given URL
        request = new WWW(path);
        yield return request;

#if UNITY_IPHONE || UNITY_android
            AudioClip audioTrack = request.GetAudioClip(false, true);
            audio.clip = audioTrack;
            audio.Play();
#endif
        
    
    void Start () 
        xRand = Random.Range(0, 20);
        if (playMusic)
             StartCoroutine(loadAndPlayAudioClip(songs[xRand]));
    
    // Update is called once per frame
    void Update () 

        if (playMusic) 
           print("Size: "+Size +" bytes");
           if (request.isDone) 
                  fileData = request.bytes;
                   Size = fileData.Length; 
                   if (fileData.Length > 0) 
                            File.WriteAllBytes(Application.persistentDataPath + "/" + currentSong, fileData);
                            print("Saving mp3 to " + Application.persistentDataPath + "/" + currentSong);
                    
           
        
    

【讨论】:

【参考方案2】:

File.ReadAllBytesImageConversion.LoadImage 均未提供任何进度反馈!

虽然第一个至少可以异步,但后者肯定必须在主线程上调用。

如果您愿意,您可以在使用UnityWebRequest.Get 进行文件读取时取得进展 - 这也可以用于硬盘驱动器上的文件。 但是,在我看来,这并不能真正给您带来任何更大的好处,因为它只会为一个文件而不是所有文件提供稍微细粒度的进度。

=>您真正可以取得进展的唯一事情是每个加载的纹理都有一个进度更新,但无论哪种方式,这肯定会使您的主线程在一定程度上停止并且运行不完全流畅。

public class StreamVideo : MonoBehaviour

    public Texture2D[] frames;    
    public float framesPerSecond = 2.0f;
    public RawImage image;
    public int currentFrameIndex;

    public GameObject LoadingText;
    public Text ProgressIndicator;
    public Image LoadingBar;
    public float speed;

    // Here adjust more or less the target FPS while loading
    // This is a trade-off between frames lagging and real-time duration for the loading
    [SerializeField] private int targetFPSWhileLoading = 30;

    private readonly ConcurrentQueue<Action> _mainThreadActions = new ConcurrentQueue<Action>();
    private readonly ConcurrentQueue<LoadArgs> _fileContents = new ConcurrentQueue<LoadArgs>();
    private int _framesLoadedAmount;
    private Thread _fileReadThread;

    private class LoadArgs
    
        public readonly int Index;
        public readonly byte[] Bytes;
        public readonly FileInfo Info;

        public LoadArgs(int index, FileInfo info, byte[] bytes)
        
            Index = index;
            Info = info;
            Bytes = bytes;
        
    

    private void Start()
    
        if (!image)
        
            //Get Raw Image Reference
            image = gameObject.GetComponent<RawImage>();
        

        StartCoroutine(LoadingRoutine());
        _fileReadThread = new Thread(ReadAllFilesInThread);
        _fileReadThread.Start();
    

    


    // This routine runs in the main thread and waits for values getting filled into 
    private IEnumerator LoadingRoutine()
    
        // start a stopwatch
        // we will use it later to try to load as many images as possible within one frame without
        // going under a certain desired frame-rate
        // If you choose it to strong it might happen that you load only one image per frame
        // => for 500 frames you might have to wait 500 frames
        var stopWatch = new Stopwatch();
        var maxDurationMilliseconds = 1000 / (float)targetFPSWhileLoading;
        stopWatch.Restart();

        // execute until all images are loaded
        while (frames.Length == 0 || currentFrameIndex < frames.Length)
        
            // little control flag -> if still false after all while loops -> wait one frame anyway to not 
            // stall the main thread
            var didSomething = false;

            while (_mainThreadActions.Count > 0 && _mainThreadActions.TryDequeue(out var action))
            
                didSomething = true;
                action?.Invoke();

                if (stopWatch.ElapsedMilliseconds > maxDurationMilliseconds)
                
                    stopWatch.Restart();
                    yield return null;
                
            

            while (_fileContents.Count > 0 && _fileContents.TryDequeue(out var args))
            
                frames[args.Index] = new Texture2D(1, 1);
                if (!frames[args.Index].LoadImage(args.Bytes, false))
                
                    Debug.LogError($"Could not load image for index args.Index from args.Info.FullName!", this);
                
                _framesLoadedAmount++;
                UpdateProgressBar();
                didSomething = true;

                if (stopWatch.ElapsedMilliseconds > maxDurationMilliseconds)
                
                    stopWatch.Restart();
                    yield return null;
                
            

            if (!didSomething)
            
                yield return null;
            
        
    

    // Runs asynchronous on a background thread -> doesn't stall the main thread
    private void ReadAllFilesInThread()
    
        var dir = new DirectoryInfo(@"C:\tmp");
        // since you use ToLower() the capitalized version are quite redundant btw ;)
        var extensions = new[]  ".jpg", ".jpeg", ".png" ;
        var fileInfos = dir.GetFiles().Where(f => extensions.Contains(f.Extension.ToLower())).ToArray();

        _mainThreadActions.Enqueue(() =>
        
            // initialize the frame array on the main thread
            frames = new Texture2D[fileInfos.Length];
        );

        for (var i = 0; i < fileInfos.Length; i++)
        
            var bytes = File.ReadAllBytes(fileInfos[i].FullName);
            // write arguments to the queue
            _fileContents.Enqueue(new LoadArgs(i, fileInfos[i], bytes));
        
    

    private void UpdateProgressBar()
    
        if (LoadingBar.fillAmount < 1f)
        
            // if not even received the fileInfo amount yet leave it at 0
            // otherwise the progress is already loaded frames divided by frames length
            LoadingBar.fillAmount = frames.Length == 0 ? 0f : _framesLoadedAmount / (float)frames.Length;
            ProgressIndicator.text = LoadingBar.fillAmount.ToString("P");
            LoadingText.SetActive(true);
        
        else
        
            LoadingText.SetActive(false);
            ProgressIndicator.text = "Done";
        
    

    private void Update()
    
        // Wait until frames are all loaded
        if (_framesLoadedAmount == 0 || _framesLoadedAmount < frames.Length)
        
            return;
        

        var index = (int)(Time.time * framesPerSecond) % frames.Length;
        image.texture = frames[index]; //Change The Image
    

    private void OnDestroy()
    
        _fileReadThread?.Abort();

        foreach (var frame in frames)
        
            Destroy(frame);
        
    

【讨论】:

效果不好。运行游戏时,径向开始需要 1-2 秒,径向速度非常快地达到 100,甚至很难看到它正在改变百分比,然后在开始显示图像之前等待几秒钟,然后是径向已经是 100%,而且它不是异步的,径向和图像处理的时间不同。

以上是关于在 Unity3D 中,如何在迭代 FileInfo[] 数组(非阻塞)时异步显示 UI 径向进度条?的主要内容,如果未能解决你的问题,请参考以下文章

yield学习续:yield return迭代块在Unity3D中的应用——协程

unity3d c#支持热更吗

Unity3D-制作火焰效果

如何在unity3d中加载大工程模型

如何在unity3D游戏引擎中写波斯语?

如何在 Unity3D 中制作平滑的十字准线?