Trabajando con recursos externos en Unity3D

Introduccion


Hola queridos lectores, hoy hablaremos sobre trabajar con recursos externos en el entorno Unity 3d.

Por tradición, para comenzar, determinaremos qué es y por qué lo necesitamos. Entonces, ¿qué son exactamente estos recursos externos? Como parte del desarrollo del juego, dichos recursos pueden ser todo lo que se requiere para que la aplicación funcione y no deben almacenarse en la compilación final del proyecto. Los recursos externos se pueden ubicar tanto en el disco duro de la computadora del usuario como en un servidor web externo. En el caso general, dichos recursos son cualquier archivo o conjunto de datos que cargamos en nuestra aplicación que ya se está ejecutando. Hablando en el marco de Unity 3d, pueden ser:

  • Archivo de texto
  • Archivo de textura
  • Archivo de audio
  • Conjunto de bytes
  • AssetBundle (archivo con activos del proyecto Unity 3d)

A continuación, examinaremos con más detalle los mecanismos integrados para trabajar con estos recursos que están presentes en Unity 3d, y también escribiremos administradores simples para interactuar con el servidor web y cargar recursos en la aplicación.

Nota : el resto de este artículo usa código usando C # 7+ y está diseñado para el compilador Roslyn usado en Unity3d en las versiones 2018.3+.

Características de la Unidad 3d


Antes de Unity 2017, se usaba un mecanismo (excluido el autodescrito) para trabajar con datos del servidor y recursos externos, que se incluía en el motor: esta es la clase WWW. Esta clase permitió el uso de varios comandos http (get, post, put, etc.) en forma síncrona o asíncrona (a través de Coroutine). Trabajar con esta clase fue bastante simple y directo.

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

Del mismo modo, puede obtener no solo datos de texto, sino también otros:


Sin embargo, a partir de la versión 2017, Unity tiene un nuevo sistema de servidor introducido por la clase UnityWebRequest , que se encuentra en el espacio de nombres de Redes. Hasta Unity 2018, existía junto con WWW , pero en la última versión del motor WWW no se recomendaba, y en el futuro se eliminará por completo. Por lo tanto, además nos centraremos solo en UnityWebRequest (en adelante, UWR).

Trabajar con UWR en su conjunto es similar a WWW en su núcleo, pero hay diferencias, que se discutirán más adelante. A continuación se muestra un ejemplo similar de carga de texto.

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

Los principales cambios que ha introducido el nuevo sistema UWR (además de los cambios en el principio de trabajar en el interior) son la capacidad de asignar controladores para cargar y descargar datos desde el servidor mismo, se pueden encontrar más detalles aquí . Por defecto, estas son las clases UploadHandler y DownloadHandler . La unidad misma proporciona un conjunto de extensiones de estas clases para trabajar con varios datos, como audio, texturas, activos, etc. Consideremos trabajar con ellos con más detalle.

Trabajar con recursos


Texto


Trabajar con texto es una de las opciones más fáciles. El método para descargarlo ya se ha descrito anteriormente. Lo reescribimos un poco con el uso de la creación de una solicitud http Get directa.

 IEnumerator LoadTextFromServer(string url, Action<string> response) { var request = UnityWebRequest.Get(url); yield return 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(); } 

Como puede ver en el código, aquí se usa el DownloadHandler predeterminado. La propiedad de texto es un captador que convierte una matriz de bytes en texto codificado UTF8. El uso principal de cargar texto desde el servidor es recibir un archivo json (representación serializada de los datos en forma de texto). Puede obtener estos datos utilizando la clase Unity JsonUtility .

 var data = JsonUtility.FromJson<T>(value); // T  ,    . 

Audio


Para trabajar con audio, debe usar el método especial de creación de la solicitud UnityWebRequestMultimedia.GetAudioClip , y para obtener la representación de datos en la forma necesaria para trabajar en Unity, debe usar DownloadHandlerAudioClip . Además, al crear una solicitud, debe especificar el tipo de datos de audio representados por la enumeración AudioType , que establece el formato (wav, aiff, oggvorbis, etc.).

 IEnumerator LoadAudioFromServer(string url, AudioType audioType, Action<AudioClip> response) { var request = UnityWebRequestMultimedia.GetAudioClip(url, audioType); yield return 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(); } 

Textura


La descarga de texturas es similar a la de los archivos de audio. La solicitud se crea utilizando UnityWebRequestTexture.GetTexture . Para obtener datos en la forma necesaria para Unity, se utiliza DownloadHandlerTexture .

 IEnumerator LoadTextureFromServer(string url, Action<Texture2D> response) { var request = UnityWebRequestTexture.GetTexture(url); yield return 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(); } 

Paquete de activos


Como se mencionó anteriormente, el paquete es, de hecho, un archivo con recursos de Unity que se puede usar en un juego que ya se está ejecutando. Estos recursos pueden ser cualquier activo del proyecto, incluidas las escenas. La excepción son los scripts de C #; no se pueden pasar. Para cargar AssetBundle , se utiliza una consulta que se crea utilizando UnityWebRequestAssetBundle.GetAssetBundle. DownloadHandlerAssetBundle se utiliza para obtener datos en la forma necesaria para Unity.

 IEnumerator LoadBundleFromServer(string url, Action<AssetBundle> response) { var request = UnityWebRequestAssetBundle.GetAssetBundle(url); yield return 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(); } 

Los principales problemas y soluciones al trabajar con un servidor web y datos externos


Los métodos simples de interacción entre una aplicación y un servidor en términos de carga de varios recursos se han descrito anteriormente. Sin embargo, en la práctica, las cosas son mucho más complicadas. Considere los principales problemas que acompañan a los desarrolladores y analice las formas de resolverlos.

No hay suficiente espacio libre


Uno de los primeros problemas al descargar datos del servidor es una posible falta de espacio libre en el dispositivo. A menudo sucede que el usuario usa dispositivos antiguos para juegos (especialmente en Android), así como el tamaño de los archivos descargados puede ser bastante grande (hola PC). En cualquier caso, esta situación debe procesarse correctamente y el jugador debe ser informado de antemano de que no hay suficiente espacio y cuánto. Como hacerlo Lo primero que debe saber es el tamaño del archivo descargado, esto se hace mediante la solicitud UnityWebRequest.Head () . A continuación se muestra el código para obtener el tamaño.

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

Es importante tener en cuenta una cosa aquí, para que la solicitud funcione correctamente, el servidor debe poder devolver el tamaño del contenido, de lo contrario (de hecho, para mostrar el progreso), se devolverá el valor incorrecto.

Después de obtener el tamaño de los datos descargados, podemos compararlo con el tamaño del espacio libre en disco. Para obtener este último, utilizo el complemento gratuito de Asset Store .

Nota : puede usar la clase Cache en Unity3d, puede mostrar espacio de caché libre y usado. Sin embargo, vale la pena considerar el punto de que estos datos son relativos. Se calculan en función del tamaño de la memoria caché, de forma predeterminada es de 4 GB. Si el usuario tiene más espacio libre que el tamaño del caché, entonces no habrá problemas, sin embargo, si esto no es así, los valores pueden tomar valores incorrectos en relación con el estado real de las cosas.

Comprobación de acceso a internet


Muy a menudo, antes de descargar cualquier cosa del servidor, es necesario manejar la situación de falta de acceso a Internet. Hay varias formas de hacer esto: desde hacer ping a una dirección, a una solicitud GET a google.ru. Sin embargo, en mi opinión, el resultado más correcto y rápido y estable es descargar desde su propio servidor (el mismo desde donde se descargarán los archivos) un archivo pequeño. Cómo hacerlo se describe arriba en la sección sobre cómo trabajar con texto.
Además de comprobar el hecho de tener acceso a Internet, también es necesario determinar su tipo (móvil o WiFi), porque es poco probable que un jugador quiera descargar varios cientos de megabytes en el tráfico móvil. Esto se puede hacer a través de la propiedad Application.internetReachability .

Almacenamiento en caché


El siguiente, y uno de los problemas más importantes, es el almacenamiento en caché de los archivos descargados. ¿Para qué es este almacenamiento en caché?

  1. Ahorre tráfico (no descargue datos ya descargados)
  2. Asegurar el trabajo en ausencia de Internet (puede mostrar datos de la memoria caché).

¿Qué necesita ser almacenado en caché? La respuesta a esta pregunta es todo, todos los archivos que descargues deben estar en caché. Cómo hacer esto, considere a continuación y comience con archivos de texto simples.
Desafortunadamente, Unity no tiene un mecanismo incorporado para almacenar texto en caché, así como texturas y archivos de audio. Por lo tanto, para estos recursos, debe escribir su sistema, o no escribir, dependiendo de las necesidades del proyecto. En el caso más simple, simplemente escribimos el archivo en el caché y, en ausencia de Internet, tomamos el archivo de él. En una versión un poco más compleja (la uso en proyectos), enviamos una solicitud al servidor, que devuelve json indicando las versiones de los archivos que están almacenados en el servidor. Puede escribir y leer archivos de la memoria caché utilizando la clase C # de la clase File o de cualquier otra manera conveniente y aceptada por su equipo.

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

Del mismo modo, obtener datos del caché.

 private string GetTextFromCache(string fileName) { var cacheFilePath = Path.Combine(Utils.Path.Cache, "{0}.text".Fmt(fileName)); if (File.Exists(cacheFilePath)) { return File.ReadAllText(cacheFilePath); } return null; } 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; } 

Nota : por qué el mismo UWR con una URL del archivo de formulario: // no se usa para cargar texturas. Por el momento, hay problemas con esto, el archivo simplemente no se carga, así que tuve que encontrar una solución.

Nota : No uso la carga directa de AudioClip en proyectos, almaceno todos esos datos en AssetBundle. Sin embargo, si es necesario, esto se puede hacer fácilmente utilizando las funciones de las clases GetData y SetData de AudioClip.

Los recursos simples de Unity para AssetBundle , Unity tienen un mecanismo de almacenamiento en caché incorporado. Consideremos con más detalle.

Básicamente, este mecanismo puede usar dos enfoques:

  1. Usando CRC y número de versión
  2. Usar valores hash

En principio, puede usar cualquiera de ellos, pero decidí por mí mismo que Hash es el más aceptable, ya que tengo mi propio sistema de versión y tiene en cuenta no solo la versión de AssetBundle , sino también la versión de la aplicación, ya que a menudo el paquete puede no ser compatible con la versión, presentado en tiendas.

Entonces, ¿cómo se hace el almacenamiento en caché?

  1. Solicitamos un archivo de paquete del servidor de manifiesto (este archivo se crea automáticamente cuando se crea y contiene una descripción de los activos que contiene, así como hash, crc, tamaño, etc.). El archivo tiene el mismo nombre que el paquete más la extensión .manifest.
  2. Obtenga el valor hash128 del manifiesto
  3. Creamos una solicitud al servidor para obtener un AssetBundle, donde además de la url, especifique el valor hash128 recibido

Código para el algoritmo descrito anteriormente:
 IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response) { // ,    while (!Caching.ready) { yield return null; } //     var request = UnityWebRequest.Get(url + ".manifest"); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { Hash128 hash = default; // hash var 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); yield return request.SendWebRequest(); if (!request.isHttpError && !request.isNetworkError) { response(DownloadHandlerAssetBundle.GetContent(request)); } else { response(null); } } else { response(null); } } else { response(null); } request.Dispose(); } 


En el ejemplo anterior, Unity, cuando consulta el servidor, primero mira para ver si hay un archivo en la caché con el valor hash128 especificado, de ser así, se devolverá; de lo contrario, se descargará el archivo actualizado. Para administrar todos los archivos de caché en Unity, hay una clase de almacenamiento en caché , con la que podemos averiguar si hay un archivo en el caché, obtener todas las versiones almacenadas en caché, así como eliminar las innecesarias o borrarlas por completo.

Nota : ¿por qué una forma tan extraña de obtener valores hash? Esto se debe al hecho de que obtener hash128 de la manera descrita en la documentación requiere cargar todo el paquete y luego recibir el activo AssetBundleManifest de él y desde allí valores hash. La desventaja de este enfoque es que todo el AssetBundle está oscilando, pero solo necesitamos que no lo sea. Por lo tanto, primero descargamos solo el archivo de manifiesto del servidor, tomamos hash128 de él, y solo entonces, si es necesario, descargamos el archivo de paquete, y el valor hash128 debe extraerse mediante la interpretación de las líneas.

Trabaja con recursos en modo editor


El último problema, o más bien la cuestión de la depuración y la conveniencia de desarrollo, es trabajar con recursos descargables en modo editor, si no hay problemas con los archivos normales, entonces las cosas no son tan simples con los paquetes. Por supuesto, puede construir su compilación cada vez, subirla al servidor e iniciar la aplicación en el editor de Unity y ver cómo funciona todo, pero incluso esta descripción suena como una "muleta". Hay que hacer algo con esto y para esto la clase AssetDatabase nos ayudará.

Para unificar el trabajo con paquetes hice un envoltorio especial:

 public class AssetBundleWrapper { private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } } 

Ahora necesitamos agregar dos modos de trabajar con activos, dependiendo de si estamos en el editor o en la compilación. Para la compilación, usamos envoltorios sobre las funciones de la clase AssetBundle , y para el editor usamos la clase AssetDatabase mencionada anteriormente.

Por lo tanto, obtenemos el siguiente código:
 public class AssetBundleWrapper { #if UNITY_EDITOR private readonly List<string> _assets; public AssetBundleWrapper(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); return string.CompareOrdinal(name, assetName) == 0; }); if (!string.IsNullOrEmpty(assetPath)) { return AssetDatabase.LoadAssetAtPath<T>(assetPath); } else { return default; } } 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(); } public void LoadAssetAsync<T>(string name, Action<T> result) where T : UnityEngine.Object { result(LoadAsset<T>(name)); } public void LoadAssetsAsync<T>(Action<T[]> result) where T : UnityEngine.Object { result(LoadAssets<T>()); } public string[] GetAllScenePaths() { return _assets.ToArray(); } public void Unload(bool includeAllLoadedAssets = false) { _assets.Clear(); } #else private readonly AssetBundle _assetBundle; public AssetBundleWrapper(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>(); } public void 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(); } public void 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(); } public string[] GetAllScenePaths() { return _assetBundle.GetAllScenePaths(); } public void Unload(bool includeAllLoadedAssets = false) { _assetBundle.Unload(includeAllLoadedAssets); } #endif } 


Nota : el código usa la clase TaskManager , se discutirá a continuación, en resumen, este es un contenedor para trabajar con Coroutine .

Además de lo anterior, también es útil durante el desarrollo ver lo que hemos descargado y lo que está actualmente en el caché. Para este propósito, puede aprovechar la capacidad de configurar su propia carpeta, que se utilizará para el almacenamiento en caché (también puede escribir texto descargado y otros archivos en la misma carpeta):

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

Escribimos un administrador de solicitudes de red o trabajamos con un servidor web


Anteriormente examinamos los aspectos principales de trabajar con recursos externos en Unity, ahora me gustaría detenerme en la implementación de la API, que generaliza y unifica todo lo anterior. Y primero, detengámonos en el administrador de consultas de red.

Nota : en lo sucesivo, usamos el contenedor sobre Coroutine en forma de la clase TaskManager . Escribí sobre este contenedor en otro artículo .

Vamos a obtener la clase correspondiente:

 public class Network { public enum NetworkTypeEnum { None, Mobile, WiFi } public static NetworkTypeEnum NetworkType; private readonly TaskManager _taskManager = new TaskManager(); } 

El campo estático NetworkType es necesario para que la aplicación reciba información sobre el tipo de conexión a Internet. En principio, este valor se puede almacenar en cualquier lugar, decidí que es el lugar en la clase de red .

Agregue la función básica de enviar una solicitud al servidor:
 private IEnumerator WebRequest(UnityWebRequest request, Action<float> progress, Action<UnityWebRequest> response) { while (!Caching.ready) { yield return null; } if (progress != null) { request.SendWebRequest(); _currentRequests.Add(request); while (!request.isDone) { progress(request.downloadProgress); yield return null; } progress(1f); } else { yield return request.SendWebRequest(); } response(request); if (_currentRequests.Contains(request)) { _currentRequests.Remove(request); } request.Dispose(); } 


Como puede ver en el código, el método para procesar la finalización de una solicitud ha cambiado en comparación con el código de las secciones anteriores. Esto es para mostrar el progreso de la carga de datos. Además, todas las solicitudes enviadas se almacenan en una lista para que, si es necesario, se puedan cancelar.

Agregue una función de creación de consultas basada en enlaces para 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); } 


Del mismo modo, las funciones se crean para textura, audio, texto, matriz de bytes.

Ahora debe asegurarse de que el servidor envíe datos a través del comando Publicar. A menudo necesita pasar algo al servidor y, dependiendo de qué exactamente, obtenga una respuesta. Agregue las funciones apropiadas.

Envío de datos en forma de un conjunto clave-valor:
 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); } 


Envío de datos como 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); } 


Ahora agregaremos métodos públicos con los que cargaremos datos, en particular AssetBundle
 public void Request(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); } 


Del mismo modo, se agregan métodos para textura, archivo de audio, texto, etc.

Y finalmente, agregamos la función de obtener el tamaño del archivo descargado y la función de limpieza para detener todas las solicitudes creadas.
 public void Request(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, out int returnValue)) { response(returnValue); } else { response(-1); } }), priority); } public void Clear() { _taskManager.Clear(); foreach (var request in _currentRequests) { request.Abort(); request.Dispose(); } _currentRequests.Clear(); } 


En esto, se completa nuestro gerente para trabajar con solicitudes de red. Si es necesario, cada subsistema del juego que requiere trabajar con el servidor puede crear sus propias instancias de la clase.

Escribimos al gerente de carga de recursos externos


Además de la clase descrita anteriormente, para trabajar completamente con datos externos, necesitamos un administrador separado que no solo descargue los datos, sino que también notifique a la aplicación sobre el inicio de la descarga, finalización, progreso, falta de espacio libre y también se ocupará de los problemas de almacenamiento en caché.

Comenzamos la clase correspondiente, que en mi caso es un singleton
 public class ExternalResourceManager { public enum ResourceEnumType { Text, Texture, AssetBundle } private readonly Network _network = new Network(); public void ExternalResourceManager() { #if UNITY_EDITOR var path = Path.Combine(Directory.GetParent(Application.dataPath).FullName, "_EditorCache"); #else var path = Path.Combine(Application.persistentDataPath, "_AppCache"); #endif if (!System.IO.Directory.Exists(path)) { System.IO.Directory.CreateDirectory(path); #if UNITY_IOS UnityEngine.iOS.Device.SetNoBackupFlag(path); #endif } Caching.currentCacheForWriting = Caching.AddCache(path); } } 


Como puede ver, el diseñador establece la carpeta para el almacenamiento en caché, dependiendo de si estamos en el editor o no. Además, configuramos un campo privado para una instancia de la clase Red, que describimos anteriormente.

Ahora agregaremos funciones auxiliares para trabajar con el caché, así como determinar el tamaño del archivo descargado y verificar el espacio libre para ello. Más adelante, el código se da en un ejemplo de trabajo con AssetBundle, para el resto de los recursos todo se hace por analogía.

Código de función auxiliar
 public void ClearAssetBundleCache(string url) { var fileName = GetFileNameFromUrl(url); Caching.ClearAllCachedVersions(fileName); } public void ClearAllRequest() { _network.Clear(); } public void AssetBundleIsCached(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); } public void CheckFreeSpace(string url, Action<bool, float> result) { GetSize(url, lengthInMb => { #if UNITY_EDITOR_WIN var logicalDrive = Path.GetPathRoot(Utils.Path.Cache); var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(logicalDrive); #elif UNITY_EDITOR_OSX var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(); #elif UNITY_IOS var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(); #elif UNITY_ANDROID var availableSpace = SimpleDiskUtils.DiskUtils.CheckAvailableSpace(true); #endif result(availableSpace > lengthInMb, lengthInMb); }); } public void GetSize(string url, Action<float> result) { _network.Request(url, length => result(length / 1048576f)); } private string GetFileNameFromUrl(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; } 


Ahora agreguemos funciones de carga de datos usando el ejemplo AssetBundle.
 public void GetAssetBundle(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(); #else void loadAssetBundle(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 } 


Entonces, ¿qué sucede en esta función?

  • La directiva de precompilación DONT_USE_SERVER_IN_EDITOR se usa para deshabilitar la carga real de paquetes desde el servidor.
  • El primer paso es hacer una solicitud al servidor para obtener el archivo de manifiesto para el paquete
  • - , , - ( _externalResourcesStorage ) , , ( , ), , null
  • , Caching , , ( )
  • , , , , - ( ). , ( )

Nota : es importante comprender por qué el valor hash se almacena por separado. Esto es necesario para el caso cuando no hay Internet, o la conexión es inestable, o hubo algún tipo de error de red y no pudimos descargar el paquete desde el servidor, en este caso garantizamos descargar el paquete desde el caché si está presente allí .

De manera similar al método descrito anteriormente, en el administrador, puede / necesita obtener otras funciones para trabajar con datos: GetJson, GetTexture, GetText, GetAudio, etc.

Y finalmente, necesita obtener un método que le permita descargar conjuntos de recursos. Este método será útil si necesitamos descargar o actualizar algo al inicio de la aplicación.
 public void GetPack(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; void progressHandler(float value) { currentProgress += value; progress?.Invoke(currentProgress / commonProgress); }; void completeHandler() { 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; } } } 


Aquí vale la pena comprender la peculiaridad del TaskManager , que se utiliza en el administrador de solicitudes de red, por defecto funciona, realizando todas las tareas a su vez. Por lo tanto, la descarga de archivos se realizará en consecuencia.

Nota : para aquellos a quienes no les gusta Coroutine , todo se puede traducir fácilmente a async / wait , pero en este caso, en el artículo, decidí usar una opción más comprensible para principiantes (como me parece).

Conclusión


En este artículo, traté de describir el trabajo con recursos externos de aplicaciones de juegos de la manera más compacta posible. Este enfoque y este código se utilizan en proyectos que se han lanzado y se están desarrollando con mi participación. Es bastante simple y aplicable en juegos simples donde no hay comunicación constante con el servidor (MMO y otros juegos complejos de f2p), pero facilita enormemente el trabajo si necesitamos descargar materiales adicionales, idiomas, implementar la validación de compras del lado del servidor y otros datos que una vez o no se usa con demasiada frecuencia en la aplicación.

Enlaces proporcionados en el artículo :
assetstore.unity.com/packages/tools/simple-disk-utils-59382
habr.com/post/352296
habr.com/post/282524

Source: https://habr.com/ru/post/es433366/


All Articles