Work with external resources in Unity3D

    Introduction


    Hello dear readers, today it will be about working with external resources in the Unity 3d environment.

    According to tradition, first we will decide what it is and why we need it. So, what are these external resources. As part of game development, such resources can be all that is required for the operation of the application and should not be stored in the final build of the project. External resources can be located either on the hard disk of the user's computer or on an external web server. In general, such resources are any file or data set that we load into our already running application. If we talk in the framework of Unity 3d, then they can be:

    • Text file
    • Texture file
    • Audio file
    • Byte array
    • AssetBundle (archive with assets of the Unity 3d project)

    Below, we will take a closer look at the built-in mechanisms for working with these resources, which are present in Unity 3d, and also write simple managers to interact with the web server and load resources into the application.

    Note : The article later uses code using C # 7+ and is designed for the Roslyn compiler used in Unity3d in versions 2018.3+.

    Unity 3d features


    Prior to the 2017 version of Unity, one mechanism was used to work with server data and external resources (excluding self-written ones), which was included in the engine - this is the WWW class. This class allowed to use various http commands (get, post, put, etc.) in a synchronous or asynchronous form (via Coroutine). Work with this class was fairly simple and straightforward.

    IEnumerator LoadFromServer(string url)
    {
         var www = new WWW(url);
         yieldreturn www;
         Debug.Log(www.text);
    }
    

    Similarly, you can get not only text data, but also others:


    However, starting with version 2017, Unity has a new system for working with the server, represented by the UnityWebRequest class , which is located in the Networking namespace. Prior to Unity 2018, it existed along with the WWW , but in the latest version of the WWW engine, it was not recommended, and will later be completely removed. Therefore, the following discussion will deal only with UnityWebRequest (hereinafter UWR).

    Working with the UWR in general is similar to the WWW at its core, but there are also differences, which will be discussed further. Below is a similar example of loading text.

    IEnumerator LoadFromServer(string url)
    {
        var request = new UnityWebRequest(url);
        yieldreturn request.SendWebRequest();
        Debug.Log(request.downloadHandler.text);
        request.Dispose();
    }
    

    The main changes introduced by the new UWR system (in addition to changes in the principle of operation inside) are the ability to assign handlers to download and download data from the server itself, read more here . By default, these are the UploadHandler and DownloadHandler classes . Unity itself provides a set of extensions of these classes for working with various data, such as audio, textures, assets, etc. Let us consider the work with them.

    Work with resources


    Text


    Working with text is one of the easiest options. Above has been described a way to download it. Let's rewrite it a bit using the creation of a direct http get request.

    IEnumerator LoadTextFromServer(string url, Action<string> response)
    {
        var request = UnityWebRequest.Get(url);
        yieldreturn request.SendWebRequest();
        if (!request.isHttpError && !request.isNetworkError)
        {
            response(uwr.downloadHandler.text);        
        }
        else
        {
        	Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error);
            response(null);
        }
        request.Dispose();
    }
    

    As you can see from the code, DownloadHandler is used here by default. The text property is a getter that converts a byte array to text in UTF8 encoding. The main use of loading text from a server is getting a json file (a serialized representation of data in text form). You can get this data using the Unity JsonUtility class .

    var data = JsonUtility.FromJson<T>(value); 
    //здесь T тип данных, которые хранятся в строке.

    Audio


    To work with audio, you need to use a special method to create a UnityWebRequestMultimedia.GetAudioClip request , and to get a view of the data you need to work in Unity, you must use DownloadHandlerAudioClip . In addition, when creating a request, you must specify the type of audio data represented by the AudioType enumeration , which specifies the format (wav, aiff, oggvorbis, etc.).

    IEnumerator LoadAudioFromServer(string url, 
                                    AudioType audioType, 
                                    Action<AudioClip> response)
    {
        var request = UnityWebRequestMultimedia.GetAudioClip(url, audioType);
        yieldreturn request.SendWebRequest();
        if (!request.isHttpError && !request.isNetworkError)
        {
        	response(DownloadHandlerAudioClip.GetContent(request));    
        }
        else
        {
        	Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error);
            response(null);
        }
        request.Dispose();
    }
    

    Texture


    Loading textures is similar to audio files. The request is created using UnityWebRequestTexture.GetTexture . To get the data in the right form for Unity, DownloadHandlerTexture is used .

    IEnumerator LoadTextureFromServer(string url, Action<Texture2D> response)
    {
        var request = UnityWebRequestTexture.GetTexture(url);
        yieldreturn request.SendWebRequest();
        if (!request.isHttpError && !request.isNetworkError)
        {
        	response(DownloadHandlerTexture.GetContent(request));
        }
        else
        {
        	Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error);
            response(null);
        }
        request.Dispose();
    }
    

    Assetbundle


    As mentioned earlier, the bundle is, in fact, an archive with Unity resources that can be used in an already running game. These resources can be any project assets, including scenes. The exception is C # scripts, they can not be transferred. To load AssetBundle, use a request that is created using UnityWebRequestAssetBundle.GetAssetBundle. To get the data in the right form for Unity, DownloadHandlerAssetBundle is used .

    IEnumerator LoadBundleFromServer(string url, Action<AssetBundle> response)
    {
        var request = UnityWebRequestAssetBundle.GetAssetBundle(url);
        yieldreturn request.SendWebRequest();
        if (!request.isHttpError && !request.isNetworkError)
        {
              response(DownloadHandlerAssetBundle.GetContent(request));
        }
        else
        {
        	Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error);
            response(null);
        }
        request.Dispose();
    }
    

    The main problems and solutions when working with a web server and external data


    Above, simple ways of application interaction with the server were described in terms of downloading various resources. However, in practice, everything is much more complicated. Consider the main problems that accompany the developers and dwell on ways to solve them.

    Not enough space


    One of the first problems when downloading data from a server is the possible lack of free space on the device. It often happens that a user uses old devices for games (especially on Android), as well as the size of downloaded files can be quite large (hi PC). In any case, this situation must be properly handled and the player should be informed in advance that there is not enough space and how much. How to do it? The first thing you need to know is the size of the file being downloaded; this is done by means of the UnityWebRequest.Head () request . Below is the code to get the size.

    IEnumerator GetConntentLength(string url, Action<int> response)
    {
       var request = UnityWebRequest.Head(url);
       yieldreturn request.SendWebRequest();
       if (!request.isHttpError && !request.isNetworkError)
       {
            var contentLength = request.GetResponseHeader("Content-Length");
            if (int.TryParse(contentLength, outint returnValue))
       	{
       	      response(returnValue);
            }
       	else
            {
       	      response(-1);
            }
        }
        else
        {
        	Debug.LogErrorFormat("error request [{0}, {1}]", url, request.error);
            response(-1);
        }
    }
    

    Here it is important to note one thing, for the request to work properly, the server should be able to return the size of the content, otherwise (as, in fact, to display progress) the wrong value will be returned.

    After we received the size of the downloaded data, we can compare it with the size of free disk space. For the latter, I use the free plugin from the Asset Store .

    Note : you can use the class Cachein unity3d, it can show free and occupied space in the cache. However, it is worth considering the moment that these data are relative. They are calculated based on the size of the cache itself, by default it is 4GB. If the user has more free space than the cache size, then there will be no problems, but if this is not the case, then the values ​​may assume incorrect values ​​relative to the real state of affairs.

    Internet access check


    Very often, before downloading something from the server, it is necessary to handle the situation of lack of access to the Internet. There are several ways to do this: from pinging an address to a GET request to google.com. However, in my opinion, the most correct and giving a fast and stable result is downloading from a small server (the same one from which the files will download). How to do this is described above in the text section.
    In addition to checking for the very fact of having access to the Internet, you must also determine its type (mobile or WiFi), because the player is unlikely to want to download several hundred megabytes on mobile traffic. This can be done through the Application.internetReachability property .

    Caching


    Next, and one of the most important problems, is the caching of downloaded files. Why do we need this caching:

    1. Saving traffic (do not download already downloaded data)
    2. Providing work in the absence of the Internet (you can show data from the cache).

    What do you need to cache? The answer to this question is everything, all the files that you download need to be cached. How to do this, consider below, and start with simple text files.
    Unfortunately, in Unity there is no built-in mechanism for caching text, as well as textures and audio files. Therefore, for these resources, you need to write your system, or not to write, depending on the needs of the project. In the simplest version, we simply write the file to the cache and in the absence of the Internet we take the file from it. In a slightly more complicated version (I use it in projects), we send a request to the server, which is returned by json with the indication of the versions of files that are stored on the server. You can write and read files from the cache using the C # File class or any other convenient method accepted by your team.

    privatevoidCacheText(string fileName, string data)
    {
        var cacheFilePath = Path.Combine("CachePath", "{0}.text".Fmt(fileName));
        File.WriteAllText(cacheFilePath, data);
    }
    privatevoidCacheTexture(string fileName, byte[] data)
    {
        var cacheFilePath = Path.Combine("CachePath", "{0}.texture".Fmt(fileName));
        File.WriteAllBytes(cacheFilePath, data);
    }
    

    Similarly, retrieving data from the cache.

    privatestringGetTextFromCache(string fileName)
    {
        var cacheFilePath = Path.Combine(Utils.Path.Cache, "{0}.text".Fmt(fileName));
        if (File.Exists(cacheFilePath))
        {
            return File.ReadAllText(cacheFilePath);
        }
        returnnull;
    }
    private Texture2D GetTextureFromCache(string fileName)
    {
        var cacheFilePath = Path.Combine(Utils.Path.Cache, "{0}.texture".Fmt(fileName));
        Texture2D texture = null;
        if (File.Exists(cacheFilePath))
        {
            var data = File.ReadAllBytes(cacheFilePath);
            texture = new Texture2D(1, 1);
            texture.LoadImage(data, true);
        }
        return texture;
    }
    

    Note : why the same UWR is not used for loading textures from a url of the form file: //. At the moment, there is a problem with this, the file just simply does not load, so I had to find a workaround.

    Note : I do not use direct loading of AudioClip in projects, I store all such data in AssetBundle. However, if necessary, this is easily done using the functions of the AudioClip class GetData and SetData.

    Unlike simple resources for AssetBundle , Unity has a built-in caching mechanism. Consider it in more detail.

    Basically, this mechanism can use two approaches:

    1. Using CRC and version numbers
    2. Using Hash Values

    In principle, you can use any of them, but I decided for myself that Hash is the most acceptable, because I have my own version system and it takes into account not only the AssetBundle version , but also the application version, since often the bundle may not be compatible with the version presented in stores.

    So, how is caching performed:

    1. We request a bundle file from the manifest server (this file is created automatically when it is created and contains a description of the assets that it contains, as well as the values ​​of hash, crc, size, etc.). The file has the same name as the bundle plus the .manifest extension.
    2. Get from hash value hash128
    3. Create a request to the server to get AssetBundle, where, in addition to the url, we specify the resulting value hash128

    The code for the algorithm described above:
    IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response)
    {
        // Ждем, готовности системы кэшированияwhile (!Caching.ready)
        {
            yieldreturnnull;
        }
        // получаем манифест с сервераvar request = UnityWebRequest.Get(url + ".manifest");
        yieldreturn request.SendWebRequest();
        if (!request.isHttpError && !request.isNetworkError)
        {
            Hash128 hash = default;
            //получаем hashvar hashRow = request.downloadHandler.text.ToString().Split("\n".ToCharArray())[5];
            hash = Hash128.Parse(hashRow.Split(':')[1].Trim());
            if (hash.isValid == true)
            {
                request.Dispose();
                request = UnityWebRequestAssetBundle.GetAssetBundle(url, hash, 0);
                yieldreturn request.SendWebRequest();
                if (!request.isHttpError && !request.isNetworkError)
                {
                    response(DownloadHandlerAssetBundle.GetContent(request));
                }
                else
                {
                    response(null);
                }
            }
            else
            {
                response(null);
            }
        }
        else
        {
            response(null);
        }
        request.Dispose();
    }
    


    In the above example, when requested by the Unity server, it first looks to see if there is a file in the cache with the specified hash128 value, if there is, it will be returned, if not, the updated file will be loaded. To manage all the cache files in Unity there is a class Caching , with which we can find out if there is a file in the cache, get all the cached versions, and also delete unnecessary ones, or clear it completely.

    Note : why such a strange way of getting hash values? This is due to the fact that getting the hash128 in the manner described in the documentation requires downloading the entire bundle and then getting AssetBundleManifest from itasset and from there hash values. The disadvantage of this approach is that the whole AssetBundle is swinging, and we just need it not to be. Therefore, we first download only the manifest file from the server, pick up the hash128 from it, and only then, if we need to download the bundle file, while having to pull out the hash128 value will have to be interpreted as strings.

    Work with resources in editor mode


    The last problem, or rather the issue of ease of debugging and development, is working with loaded resources in editor mode, if there are no problems with regular files, then with bundles is not so simple. You can, of course, do their builds every time, upload them to the server and launch the application in the Unity editor and see how everything works, but it sounds like a “crutch” even by description. We need to do something with this, and the AssetDatabase class will help us for this .

    In order to unify the work with the bundles, I made a special wrapper:

    publicclassAssetBundleWrapper
    {
        privatereadonly AssetBundle _assetBundle;
        publicAssetBundleWrapper(AssetBundle assetBundle)
        {
             _assetBundle = assetBundle;
        }    
    }
    

    Now we need to add two modes of work with assets depending on whether we are in the editor or in the build. For the build, we use wrappers for AssetBundle class functions , and for the editor we use the AssetDatabase class mentioned above .

    Thus we get the following code:
    publicclassAssetBundleWrapper
    {
    #if UNITY_EDITORprivatereadonly List<string> _assets;
            publicAssetBundleWrapper(string url)
            {
                var uri = new Uri(url);
                var bundleName = Path.GetFileNameWithoutExtension(uri.LocalPath);
                _assets = new List<string>(AssetDatabase.GetAssetPathsFromAssetBundle(bundleName));           
            }
            public T LoadAsset<T>(string name) where T : UnityEngine.Object
            {
                var assetPath = _assets.Find(item =>
                {
                    var assetName = Path.GetFileNameWithoutExtension(item);
                    returnstring.CompareOrdinal(name, assetName) == 0;                
                });
                if (!string.IsNullOrEmpty(assetPath))
                {
                    return AssetDatabase.LoadAssetAtPath<T>(assetPath);
                } else
                {
                    returndefault;
                }
            }
            public T[] LoadAssets<T>() where T : UnityEngine.Object
            {
                var returnedValues = new List<T>();
                foreach(var assetPath in _assets)
                {
                    returnedValues.Add(AssetDatabase.LoadAssetAtPath<T>(assetPath));
                }
                return returnedValues.ToArray();
            }
            publicvoid LoadAssetAsync<T>(string name, Action<T> result) where T : UnityEngine.Object
            {
                result(LoadAsset<T>(name));
            }
            publicvoid LoadAssetsAsync<T>(Action<T[]> result) where T : UnityEngine.Object
            {
                result(LoadAssets<T>());
            }
            publicstring[] GetAllScenePaths()
            {
                return _assets.ToArray();
            }
            publicvoidUnload(bool includeAllLoadedAssets = false)
            {
                _assets.Clear();
            }
    #elseprivatereadonly AssetBundle _assetBundle;
        publicAssetBundleWrapper(AssetBundle assetBundle)
        {
            _assetBundle = assetBundle;
        }
        public T LoadAsset<T>(string name) where T : UnityEngine.Object
        {
            return _assetBundle.LoadAsset<T>(name);
        }
        public T[] LoadAssets<T>() where T : UnityEngine.Object
        {
            return _assetBundle.LoadAllAssets<T>();
        }
        publicvoid LoadAssetAsync<T>(string name, Action<T> result) where T : UnityEngine.Object
        {
            var request = _assetBundle.LoadAssetAsync<T>(name);
            TaskManager.Task.Create(request)
                            .Subscribe(() =>
                            {
                                result(request.asset as T);
                                Unload(false);
                            })
                            .Start();
        }
        publicvoid LoadAssetsAsync<T>(Action<T[]> result) where T : UnityEngine.Object
        {
            var request = _assetBundle.LoadAllAssetsAsync<T>();
            TaskManager.Task.Create(request)
                            .Subscribe(() =>
                            {
                                var assets = new T[request.allAssets.Length];
                                for (var i = 0; i < request.allAssets.Length; i++)
                                {
                                    assets[i] = request.allAssets[i] as T;
                                }
                                result(assets);
                                Unload(false);
                            })
                            .Start();
        }
        publicstring[] GetAllScenePaths()
        {
            return _assetBundle.GetAllScenePaths();
        }
        publicvoidUnload(bool includeAllLoadedAssets = false)
        {
            _assetBundle.Unload(includeAllLoadedAssets);
        }
    #endif
    }
    


    Note : the code uses the TaskManager class , it will be discussed below, if briefly, this is a wrapper for working with Coroutine .

    In addition to the above, it is also useful during development to look at what we downloaded and what is now in the cache. For this purpose, you can use the option of installing your own folder that will be used for caching (you can also write downloaded text and other files to the same folder):

    #if UNITY_EDITORvar path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "_EditorCache");
    #elsevar path = Path.Combine(Application.persistentDataPath, "_AppCache");                                
    #endif
    Caching.currentCacheForWriting = Caching.AddCache(path);
    

    Writing a network request manager or working with a web server


    Above, we covered the main aspects of working with external resources in Unity, now I would like to dwell on the implementation of the API, which summarizes and unifies all of the above. And first we will focus on the network request manager.

    Note : hereinafter the wrapper is used over Coroutine in the form of TaskManager class . I wrote about this wrapper in another article .

    Let's get the appropriate class:

    publicclassNetwork
    {        
            publicenum NetworkTypeEnum
            {
                None,
                Mobile,
                WiFi
            }
            publicstatic NetworkTypeEnum NetworkType;
            privatereadonly TaskManager _taskManager = new TaskManager();  
    }
    

    The static NetworkType field is required for the application to receive information about the type of Internet connection. In principle, this value can be stored anywhere, I decided that in the Network class it belongs to it.

    Add the basic function of sending a request to the server:
    private IEnumerator WebRequest(UnityWebRequest request, Action<float> progress, Action<UnityWebRequest> response)
    {
        while (!Caching.ready)
        {
            yieldreturnnull;
        }
        if (progress != null)
        {
            request.SendWebRequest(); _currentRequests.Add(request);
            while (!request.isDone)
            {
                progress(request.downloadProgress);
                yieldreturnnull;
            }
            progress(1f);
        }
        else
        {
            yieldreturn request.SendWebRequest();
        }
        response(request);
        if (_currentRequests.Contains(request))
        {
            _currentRequests.Remove(request);
        }
        request.Dispose();
    }
    


    As can be seen from the code, the method of processing the completion of the request is changed, compared to the code in the previous sections. This is done to show the progress of data loading. Also, all sent requests are saved in the list so that, if necessary, they can be canceled.

    Add a request creation function based on the link for AssetBundle:
    private IEnumerator WebRequestBundle(string url, Hash128 hash, Action<float> progress, Action<UnityWebRequest> response)
    {
        var request = UnityWebRequestAssetBundle.GetAssetBundle(url, hash, 0);
        return WebRequest(request, progress, response);
    }
    


    Similarly, functions for texture, audio, text, byte array are created.

    Now you need to ensure that the server sends data through the Post command. Often you need to send something to the server, and depending on what it is, get an answer. Add the appropriate functions.

    Sending data as a set of key-value:
    private IEnumerator WebRequestPost(string url, Dictionary<string, string> formFields, Action<float> progress, Action<UnityWebRequest> response)
    {
        var request = UnityWebRequest.Post(url, formFields);
        return WebRequest(request, progress, response);
    }
    


    Sending data in the form of json:
    private IEnumerator WebRequestPost(string url, string data, Action<float> progress, Action<UnityWebRequest> response)
    {
        var request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST)
        {
            uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(data)),
            downloadHandler = new DownloadHandlerBuffer()
        };
        request.uploadHandler.contentType = "application/json";
        return WebRequest(request, progress, response);
    }
    


    Now let's add public methods with the help of which we will load the data, in particular AssetBundle
    publicvoidRequest(string url, Hash128 hash, Action<float> progress, Action<AssetBundle> response, TaskManager.TaskPriorityEnum priority = TaskManager.TaskPriorityEnum.Default)
    {
            _taskManager.AddTask(WebRequestBundle(url, hash, progress, (uwr) =>
            {
                if (!uwr.isHttpError && !uwr.isNetworkError)
                {
                    response(DownloadHandlerAssetBundle.GetContent(uwr));
                }
                else
                {
                    Debug.LogWarningFormat("[Netowrk]: error request [{0}]", uwr.error);
                    response(null);
                }
            }), priority);
    }
    


    Similarly, methods are added for texture, audio file, text, etc.

    And finally, we add the function of getting the size of the downloaded file and the cleaning function, to stop all the requests that have been created.
    publicvoidRequest(string url, Action<int> response, TaskManager.TaskPriorityEnum priority = TaskManager.TaskPriorityEnum.Default)
    {
        var request = UnityWebRequest.Head(url);
            _taskManager.AddTask(WebRequest(request, null, uwr =>
            {
                var contentLength = uwr.GetResponseHeader("Content-Length");
                if (int.TryParse(contentLength, outint returnValue))
                {
                    response(returnValue);
                }
                else
                {
                    response(-1);
                }
            }), priority);
    }
    publicvoidClear()
    {
        _taskManager.Clear();
        foreach (var request in _currentRequests)
        {
            request.Abort();
            request.Dispose();
        }
        _currentRequests.Clear();    
    }
    


    This is where our network request manager is complete. Of necessity, each game subsystem that requires working with a server can create its own class instances.

    We write the manager of loading of external resources


    In addition to the class described above, in order to fully work with external data, we need a separate manager who will not only download data, but also notify the application about the start of loading, completion, progress, lack of free space, and also deal with caching issues.

    We get the appropriate class, which in my case is a singleton
    publicclassExternalResourceManager
    {
        publicenum ResourceEnumType
        {
            Text,
            Texture,
            AssetBundle
        }
        privatereadonly Network _network = new Network();
        publicvoidExternalResourceManager()
        {
    #if UNITY_EDITORvar path = Path.Combine(Directory.GetParent(Application.dataPath).FullName,   "_EditorCache");
    #elsevar path = Path.Combine(Application.persistentDataPath, "_AppCache");                                
    #endifif (!System.IO.Directory.Exists(path))
           {
               System.IO.Directory.CreateDirectory(path);
               #if UNITY_IOS
    	    UnityEngine.iOS.Device.SetNoBackupFlag(path);			     		  
               #endif
           }
           Caching.currentCacheForWriting = Caching.AddCache(path);
        }
    }
    


    As you can see, the constructor sets the folder for caching, depending on whether we are in the editor or not. Also, we set up a private field for an instance of the Network class, which we described earlier.

    Now we add support functions for working with the cache, as well as determining the size of the downloaded file and checking the free space for it. Further and below, the code is given on the example of working with AssetBundle, for other resources, everything is done by analogy.

    Supporting code
    publicvoidClearAssetBundleCache(string url)
    {
        var fileName = GetFileNameFromUrl(url);            
         Caching.ClearAllCachedVersions(fileName);
    }
    publicvoidClearAllRequest()
    {
        _network.Clear();
    }
    publicvoidAssetBundleIsCached(string url, Action<bool> result)
    {
    var manifestFileUrl = "{0}.manifest".Fmt(url);
    _network.Request(manifestFileUrl, null, (string manifest) =>
    {
                    var hash = string.IsNullOrEmpty(manifest) ? default : GetHashFromManifest(manifest);
                    result(Caching.IsVersionCached(url, hash));
    } , 
    TaskManager.TaskPriorityEnum.RunOutQueue);
    }
    publicvoidCheckFreeSpace(string url, Action<bool, float> result)
    {
        GetSize(url, lengthInMb =>
        {
    #if UNITY_EDITOR_WINvar logicalDrive = Path.GetPathRoot(Utils.Path.Cache);
            var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(logicalDrive);
    #elif UNITY_EDITOR_OSXvar availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace();
    #elif UNITY_IOSvar availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace();
    #elif UNITY_ANDROIDvar availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(true);
    #endif
            result(availableSpace > lengthInMb, lengthInMb);
        });
    }
    publicvoidGetSize(string url, Action<float> result)
    {
        _network.Request(url, length => result(length / 1048576f));
    }
    privatestringGetFileNameFromUrl(string url)
    {
        var uri = new Uri(url);
        var fileName = Path.GetFileNameWithoutExtension(uri.LocalPath);
        return fileName;
    }
    private Hash128 GetHashFromManifest(string manifest)
    {
        var hashRow = manifest.Split("\n".ToCharArray())[5];
        var hash = Hash128.Parse(hashRow.Split(':')[1].Trim());
        return hash;
    }
    


    Now let's add data loading functions on the example of AssetBundle
    publicvoidGetAssetBundle(string url,
                               Action start,
                               Action<float> progress,
                               Action stop,
                               Action<AssetBundleWrapper> result,
                               TaskManager.TaskPriorityEnum taskPriority = TaskManager.TaskPriorityEnum.Default)
    {
    #if DONT_USE_SERVER_IN_EDITOR
        start?.Invoke();
        result(new AssetBundleWrapper(url));
        stop?.Invoke();
    #elsevoidloadAssetBundle(Hash128 bundleHash)
    {
        start?.Invoke();
        _network.Request(url, bundleHash, progress,
        (AssetBundle value) =>
        {   
            if(value != null)
            {
                _externalResourcesStorage.SetCachedHash(url, bundleHash);
            }
            result(new AssetBundleWrapper(value));
            stop?.Invoke();
        }, taskPriority);
    };
    var manifestFileUrl = "{0}.manifest".Fmt(url);
    _network.Request(manifestFileUrl, null, (string manifest) =>
    {
        var hash = string.IsNullOrEmpty(manifest) ? default : GetHashFromManifest(manifest);                                
        if (!hash.isValid || hash == default)
        {
            hash = _externalResourcesStorage.GetCachedHash(url);                    
            if (!hash.isValid || hash == default)
            {
                result(new AssetBundleWrapper(null));
            }
            else
            {
                loadAssetBundle(hash);
            }
        }
        else
        {                    
            if (Caching.IsVersionCached(url, hash))
            {
                loadAssetBundle(hash);
            }
            else
            {
                CheckFreeSpace(url, (spaceAvailable, length) =>
                {
                    if (spaceAvailable)
                    {
                        loadAssetBundle(hash);
                    }
                    else
                    {
                         result(new AssetBundleWrapper(null));
                        NotEnoughDiskSpace.Call();
                    }
                 });
             }
        }
    #endif
    }
    


    So, what happens in this function:

    • The precompile directive DONT_USE_SERVER_IN_EDITOR is used to disable the actual loading of bundles from the server.
    • The first step is to query the server to get the manifest file for the bundle.
    • Then we get the hash value and check its validity, in case of failure, see if there is a hash value in the database ( _externalResourcesStorage ) for the bundle, if there is, then take it and execute the request to load the bundle without checking for free space (in this case , the bundle will be taken from the cache), if not, then return null value
    • If the previous item is not relevant, then we check through the Caching class whether the bundle file we want to download is in the cache and, if so, we execute the query without checking for free space (the file has already been downloaded)
    • If the file is not in the cache, we check the availability of free space and, if there is enough of it, send a request to get the bundle directly indicating the previously obtained hash value and store this value in the database (only after the actual load). If there is no place, then we clear the list of all requests and send a message to the system in any way (you can read about it in the corresponding article )

    Note : it is important to understand why the hash value is stored separately. This is necessary for the case when there is no Internet, or the connection is unstable, or some network error has occurred and we could not download the bundle from the server, in this case we guarantee loading the bundle from the cache, if it is present there .

    Similar to the method described above in the manager, you can / need to have other data manipulation functions: GetJson, GetTexture, GetText, GetAudio, etc.

    And finally, you need to have a method that allows you to download sets of resources. This method will be useful if we need to download or update something at the start of the application.
    publicvoidGetPack(Dictionary<string, ResourceEnumType> urls, 
                                Action start,
                                Action<float> progress,
                                Action stop, Action<string, object, bool> result)
    {            
        var commonProgress = (float)urls.Count;
        var currentProgress = 0f;
        var completeCounter = 0;
        voidprogressHandler(floatvalue)            
        {
            currentProgress += value;                
            progress?.Invoke(currentProgress / commonProgress);                
        };
        voidcompleteHandler()
        {
            completeCounter++;
            if (completeCounter == urls.Count)
            {
                stop?.Invoke();
            }
        };
        start?.Invoke();
        foreach (var url in urls.Keys)
        {
            var resourceType = urls[url];
            switch (resourceType)
            {
                case ResourceEnumType.Text:
                    {
                        GetText(url, null, progressHandler, completeHandler,
                                        (value, isCached) =>
                                        {
                                            result(url, value, isCached);
                                        });
                    }
                    break;
                case ResourceEnumType.Texture:
                    {
                        GetTexture(url, null, progressHandler, completeHandler,
                                           (value, isCached) =>
                                           {
                                               result(url, value, isCached);
                                           });
                    }
                    break;
                case ResourceEnumType.AssetBundle:
                    {
                        GetAssetBundle(url, null, progressHandler, completeHandler,
                                               (value) =>
                                               {
                                                   result(url, value, false);
                                               });
                    }
                    break;
            }
        }
    }
    


    It is worthwhile to understand the TaskManager feature that is used in the network request manager, by default it works by performing all the tasks in turn. Therefore, file uploads will occur accordingly.

    Note : for those who do not like Coroutine , everything can be quite easily translated to async / await , but in this case, in the article I decided to use a more understandable option for beginners (I think).

    Conclusion


    In this article, I tried to describe as compactly as possible work with external resources of gaming applications. This approach and code is used in projects that have been released and are being developed with my participation. It is quite simple and applicable in simple games where there is no constant communication with the server (MMO and other complex f2p games), but it greatly facilitates the work, if we need to download additional materials, languages, perform server validation of purchases and other data at the same time or not too often used in the application.

    The links mentioned in the article :
    assetstore.unity.com/packages/tools/simple-disk-utils-59382
    habr.com/post/352296
    habr.com/post/282524

    Also popular now: