Downloading Files in Parallel using Coroutines in Unity

We’re going to create a script that, when given a collection of URLs, will download the files at those URLs in parallel.

This is great for downloading non-Unity specific files like JSONs. If you want to download, say, a game object or scene you would need to look into Asset Bundles. This article doesn’t cover Asset Bundles as that is an entire topic of its own.

Let’s get started.

The Script

Let’s start with the entry point of this class, the Download method.

We’re simply taking accepting a collection of URLs (this can be a list for example) as well as a path to where we’d like to save the downloads. This path is relative to Application.persistentDataPath (this location is different based on your platform).

    [SerializeField]
    private int maxConcurrentDownloads = 3;

    private Queue<string> urlsToDownload = null;

    public void Download(IEnumerable fileURLs, string pathToStore)
    {
        urlsToDownload = new Queue<string>();

        foreach (string url in fileURLs)
            urlsToDownload.Enqueue(url);

        StartCoroutine(DownloadFiles(pathToStore));
    }    

    public void Download(IEnumerable fileURLs, string pathToStore)
    {
        urlsToDownload = new Queue<string>();

        foreach (string url in fileURLs)
            urlsToDownload.Enqueue(url);

        StartCoroutine(DownloadFiles(pathToStore));
    }

The Download Files coroutine is where we do the heavy lifting. We simply create a bunch of Unity Web Requests (based on the collection of URLs we were given) at once. We can control the number of concurrent downloads by setting maxConcurrentDownloads.

    private IEnumerator DownloadFiles(string pathToStore)
    {
        var requests = new List<UnityWebRequestAsyncOperation>();

        int requestsSent = 0;
            
        //Start all requests
        while (requestsSent < maxConcurrentDownloads)
        {
            if (urlsToDownload.Count == 0)
                break;

            var www = UnityWebRequest.Get(urlsToDownload.Dequeue());
            requests.Add(www.SendWebRequest());

            requestsSent++;
        }

        //Wait for all requests
        yield return new WaitUntil(() => AllRequestsDone(requests));

        //Process results
        HandleAllRequestsWhenFinished(requests, pathToStore);

    }

Once the requests are all sent, we WaitUntil (aptly named) the requests are done. We yield here as we’d like to make sure that the download has actually finished.

Once downloaded, we handle the requests. We simply loop through the requests and throw any out that have an error (detected via RequestHasError).

WriteToDisk takes the request and writes the bytes inside of it into the pathToStore directory we specified at the very beginning.

In the All Requests Done method we are using LINQ to utilize the ‘All’ syntax. It’s very neat!

    private bool AllRequestsDone(List<UnityWebRequestAsyncOperation> requests)
    {
        return requests.All(r => r.isDone);
    }

Multi File Downloader Usage

In a real setting I would write an interface to expose the Download() method to the rest of the game. For sake of simplicity I will reference the concrete MultiFileDownloader class.

This example downloads images from a college programming course’s website (I am not affiliated with them but it was the first link when I googled ‘downloadable sample images’).

I am simply taking a base URL and concatenating it with a list of file names. I then pass this list of URLs to the multi file downloader service.


using System.Collections.Generic;
using UnityEngine;

public class DownloadFilesExample : MonoBehaviour
{
    [SerializeField]
    private MultiFileDownloader downloader;

    [Tooltip("Application.persistentDataPath/storagePath")]
    [SerializeField]
    private string storagePath = "downloads";

    [SerializeField]
    private string baseImageUrl = "https://homepages.cae.wisc.edu/~ece533/images/";
    [SerializeField]
    private List fileNames = new List { "airplane.png", 
                                                        "arctichare.png", 
                                                        "baboon.png", 
                                                        "boat.png", 
                                                        "cat.png", 
                                                        "fruits.png", 
                                                        "girl.png", 
                                                        "goldhill.png", 
                                                        "lena.png", 
                                                        "monarch.png", 
                                                        "peppers.png", 
                                                        "pool.png" };

    void Start()
    {
        StartDownload();
    }

    public void StartDownload()
    {
        var files = PrepareUrls();
        downloader.Download(files, storagePath);
    }

    private List PrepareUrls()
    {
        var urls = new List();

        foreach (var name in fileNames)
            urls.Add(baseImageUrl + name);

        return urls;
    }
}
On Windows the files will be written to <UserName>/AppData/LocalLow/<Company>/<ProjectName>/downloads

Grab the Entire Script

You can also download an example project that includes the downloader as well as the usage example at the end.

Here’s the entire Multi File Downloader script:

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.Networking;

public class MultiFileDownloader : MonoBehaviour
{
    [SerializeField]
    private int maxConcurrentDownloads = 3;

    private Queue<string> urlsToDownload = null;

    public void Download(IEnumerable fileURLs, string pathToStore)
    {
        urlsToDownload = new Queue<string>();

        foreach (string url in fileURLs)
            urlsToDownload.Enqueue(url);

        StartCoroutine(DownloadFiles(pathToStore));
    }

    private IEnumerator DownloadFiles(string pathToStore)
    {
        var requests = new List<UnityWebRequestAsyncOperation>();

        int requestsSent = 0;
            
        //Start all requests
        while (requestsSent < maxConcurrentDownloads)
        {
            if (urlsToDownload.Count == 0)
                break;

            var www = UnityWebRequest.Get(urlsToDownload.Dequeue());
            requests.Add(www.SendWebRequest());

            requestsSent++;
        }

        //Wait for all requests
        yield return new WaitUntil(() => AllRequestsDone(requests));

        //Process results
        HandleAllRequestsWhenFinished(requests, pathToStore);

    }

    private bool AllRequestsDone(List<UnityWebRequestAsyncOperation> requests)
    {
        return requests.All(r => r.isDone);
    }

    private void HandleAllRequestsWhenFinished(List<UnityWebRequestAsyncOperation> requests, string pathToStore)
    {
        foreach (var request in requests)
        {
            if (RequestHasError(request))
                continue;   //Toss failed downloads

            WriteToDisk(request, pathToStore);
        }

        //Get more downloads
        if (urlsToDownload.Count > 0)
            StartCoroutine(DownloadFiles(pathToStore));
    }

    private bool RequestHasError(UnityWebRequestAsyncOperation request)
    {
        var error = request.webRequest.error;

        if (string.IsNullOrEmpty(error))
            return false;
        else
        {
            Debug.LogWarning(request.webRequest.url + " | " + error);
            return true;
        }
    }

    private void WriteToDisk(UnityWebRequestAsyncOperation request, string pathToStore)
    {
        var bytes = request.webRequest.downloadHandler.data;

        if (bytes != null)
        {
            var baseStorePath = CreateDirectory(pathToStore);
            var fileName = Path.GetFileName(request.webRequest.url);
            var dataPath = baseStorePath + "/" + fileName;

            File.WriteAllBytes(dataPath, bytes);

            Debug.Log("Downloaded Successful | Saved to: " + dataPath);
        }
    }

    private string CreateDirectory(string pathToStore)
    {
        var baseStorePath = Application.persistentDataPath + "/" + pathToStore;
        Directory.CreateDirectory(baseStorePath);

        return baseStorePath;
    }
}

Download an Example Project

I’ve provided a Unity package with both a demo scene and the downloader scripts. Feel free to build from it.

Download: [MultiFileDownloaderExample.unitypackage]

Content within this package.