In Unity3D, how can I asynchronously display a UI radial progress bar while iterating a FileInfo[] array (non-blocking)?
When I’m running the game the part of the GetTextures method is taking time and the game is freezing until it’s loading all the images. I want it to show the loop progress in the radial progressbar, possibly using a Coroutine on a different thread to not block the main thread.
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); } }
Now just for testing I added some code for the radial progressbar in the Update :
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;
but it also start only after the GetTextures method finish it’s operation. the main goal is to show the progress of the operation in the GetTextures in the radial progressbar.
Sometime ago I had written a Music Downloader and Player for Unity3D using WWW and byte arrays which runs without blocking the main thread and uses FileInfo.
The class creates a WWW request to download an mp3 from a public string[] songs and when the WWW type (request.isDone) it’ll File.WriteAllBytes to your local game/application path (and alas play the AudioClip). I’m wondering if you can retrofit this code so you can use your c:\tmp\ and somehow get the *.jpeg, *.jpg, *.png and persist it as desired and update your progress without blocking. You’d have to use the file://c:/tmp in path string:
request = new WWW(path);
The code below in lieu of jpg, jpeg, and pngs saves the .mp3 (.au or .wav could work) to Application.persistentDataPath using File.WriteAllBytes and plays ultimately as an audioClip. Since it’s using Coroutines there’s no main thread blocking, therefore no freezing for user. You’d of course need to retrofit this code to persist your images as Sprites or Raw Image types. I am unsure if it’ll meet your needs or can work, but if you can use any of this code and use it to not block the main thread in any capacity, i’ll be glad.
Code below is ~ 5 years old, so apologies if some classes may be deprecated.
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); } } } } }
Neither File.ReadAllBytes
nor ImageConversion.LoadImage
provide any progress feedback!
While the first can be at least made asynchronous the latter definitely has to be called on the main thread.
If you wanted you could make a progress when using UnityWebRequest.Get
for he file reading – this can also be done for files on the harddrive. However, in my opinion this can’t really give you any bigger benefit since it would only provide a slightly finer grained prorgess for one file but not for all files.
=> The only thing you really could make a progress for is having one progress update per loaded texture but either way this will definitely stall your main thread to a certain extend and not run totally smooth.
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); } } }