Как в Unity3D асинхронно отображать радиальный индикатор выполнения пользовательского интерфейса при повторении массива FileInfo [] (без блокировки)? - PullRequest
0 голосов
/ 13 июля 2020

Когда я запускаю игру, часть метода GetTextures требует времени, и игра зависает, пока не загрузит все изображения. Я хочу, чтобы он показывал прогресс l oop на радиальном индикаторе выполнения, возможно, используя сопрограмму в другом потоке, чтобы не блокировать основной поток.

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;

, но она также запускается только после завершения sh метода GetTextures его операции. основная цель - показать ход выполнения операции в GetTexture на радиальном индикаторе выполнения.

Радиальный в редакторе

Ответы [ 2 ]

0 голосов
/ 13 июля 2020

Иногда a go Я написал Musi c Downloader и Player для Unity3D, используя WWW and байтовые массивы, которые работают без блокировки основного потока и используют FileInfo.

Класс создает WWW request для загрузки mp3 из публикуемых песен c string [] и когда WWW type (request.isDone) он будет File.WriteAllBytes в вашей локальной игре / пути приложения (и, увы, воспроизвести AudioClip). Мне интересно, можете ли вы модифицировать этот код, чтобы вы могли использовать свой c: \ tmp \ и каким-то образом получить * .jpeg, * .jpg, * .png и сохранить его по желанию и обновить свой прогресс без блокировки. Вам нужно будет использовать файл: // c: / tmp в строке пути:

request = new WWW(path);

Приведенный ниже код вместо jpg, jpeg и pngs сохраняет .mp3 (могут работать .au или .wav) в Application.persistentDataPath с помощью File.WriteAllBytes и в конечном итоге воспроизводится как audioClip. Поскольку он использует сопрограммы, нет блокировки основного потока, поэтому нет зависания для пользователя. Вам, конечно, потребуется модифицировать этот код, чтобы ваши изображения сохранялись в виде спрайтов или необработанных изображений. Я не уверен, будет ли он соответствовать вашим потребностям или может ли он работать, но если вы можете использовать любой из этого кода и использовать его, чтобы не блокировать основной поток в какой-либо степени, я буду рад.

Код ниже ~ 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);
                    }
           }
        }
    }
}
0 голосов
/ 13 июля 2020

Ни File.ReadAllBytes, ни ImageConversion.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);
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...