Utilisation de ressources externes dans Unity3D

Présentation


Bonjour chers lecteurs, nous allons parler aujourd'hui de travailler avec des ressources externes dans l'environnement Unity 3d.

Par tradition, pour commencer, nous dĂ©terminerons ce que c'est et pourquoi nous en avons besoin. Alors, quelles sont exactement ces ressources externes. Dans le cadre du dĂ©veloppement de jeux, ces ressources peuvent ĂȘtre tout ce qui est nĂ©cessaire au fonctionnement de l'application et ne doivent pas ĂȘtre stockĂ©es dans la version finale du projet. Les ressources externes peuvent ĂȘtre situĂ©es Ă  la fois sur le disque dur de l'ordinateur de l'utilisateur et sur un serveur Web externe. Dans le cas gĂ©nĂ©ral, ces ressources sont tout fichier ou ensemble de donnĂ©es que nous chargeons dans notre application dĂ©jĂ  en cours d'exĂ©cution. S'exprimant dans le cadre d'Unity 3d, alors ils peuvent ĂȘtre:

  • Fichier texte
  • Fichier de texture
  • Fichier audio
  • Tableau d'octets
  • AssetBundle (archive avec les actifs du projet Unity 3d)

Ci-dessous, nous examinerons plus en détail les mécanismes intégrés pour travailler avec ces ressources qui sont présents dans Unity 3d, ainsi que pour écrire des gestionnaires simples pour interagir avec le serveur Web et charger des ressources dans l'application.

Remarque : le reste de cet article utilise du code utilisant C # 7+ et est conçu pour le compilateur Roslyn utilisé dans Unity3d dans les versions 2018.3+.

Caractéristiques de Unity 3D


Avant Unity 2017, un mécanisme (à l'exclusion de l'auto-description) était utilisé pour travailler avec les données du serveur et les ressources externes, qui étaient incluses dans le moteur - il s'agit de la classe WWW. Cette classe a permis l'utilisation de diverses commandes http (get, post, put, etc.) sous forme synchrone ou asynchrone (via Coroutine). Le travail avec cette classe était assez simple et direct.

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

De mĂȘme, vous pouvez obtenir non seulement des donnĂ©es texte, mais Ă©galement d'autres:


Cependant, à partir de la version 2017, Unity dispose d'un nouveau systÚme de serveur introduit par la classe UnityWebRequest , qui se trouve dans l'espace de noms Networking. Jusqu'à Unity 2018, il existait avec WWW , mais dans la derniÚre version du moteur WWW , il n'était plus recommandé, et à l'avenir, il sera complÚtement supprimé. Par conséquent, nous nous concentrerons davantage sur UnityWebRequest (ci-aprÚs UWR).

Travailler avec UWR dans son ensemble est similaire Ă  WWW dans son cƓur, mais il existe des diffĂ©rences, qui seront discutĂ©es plus tard. Voici un exemple similaire de chargement de texte.

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

Les principaux changements que le nouveau systĂšme UWR a introduits (en plus de changer le principe de travailler Ă  l'intĂ©rieur) sont la possibilitĂ© d'affecter des gestionnaires pour tĂ©lĂ©charger et tĂ©lĂ©charger des donnĂ©es depuis le serveur lui-mĂȘme, plus de dĂ©tails peuvent ĂȘtre lus ici . Par dĂ©faut, ce sont les classes UploadHandler et DownloadHandler . Unity lui-mĂȘme fournit un ensemble d'extensions de ces classes pour travailler avec diverses donnĂ©es, telles que l'audio, les textures, les actifs, etc. Envisageons de travailler avec eux plus en dĂ©tail.

Travailler avec des ressources


Texte


Travailler avec du texte est l'une des options les plus simples. La mĂ©thode de tĂ©lĂ©chargement a dĂ©jĂ  Ă©tĂ© dĂ©crite ci-dessus. Nous le rĂ©Ă©crivons un peu en utilisant la crĂ©ation d'une requĂȘte http Get directe.

 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(); } 

Comme vous pouvez le voir dans le code, le DownloadHandler par défaut est utilisé ici. La propriété text est un getter qui convertit un tableau d'octets en texte codé UTF8. L'utilisation principale du chargement de texte à partir du serveur est de recevoir un fichier json (représentation sérialisée des données sous forme de texte). Vous pouvez obtenir ces données à l'aide de la classe Unity JsonUtility .

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

Audio


Pour travailler avec l'audio, vous devez utiliser la méthode spéciale de création de la demande UnityWebRequestMultimedia.GetAudioClip , et pour obtenir la représentation des données sous la forme nécessaire pour travailler dans Unity, vous devez utiliser DownloadHandlerAudioClip . De plus, lors de la création d'une demande, vous devez spécifier le type de données audio représenté par l'énumération AudioType , qui définit le format (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(); } 

La texture


Le téléchargement des textures est similaire à celui des fichiers audio. La demande est créée à l'aide de UnityWebRequestTexture.GetTexture . Pour obtenir les données sous la forme nécessaire pour Unity, DownloadHandlerTexture est utilisé.

 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(); } 

Ensemble d'actifs


Comme mentionnĂ© prĂ©cĂ©demment, le bundle est, en fait, une archive avec des ressources Unity qui peuvent ĂȘtre utilisĂ©es dans un jeu dĂ©jĂ  en cours d'exĂ©cution. Ces ressources peuvent ĂȘtre n'importe quel actif du projet, y compris des scĂšnes. L'exception concerne les scripts C #, ils ne peuvent pas ĂȘtre transmis. Pour charger le AssetBundle , une requĂȘte est utilisĂ©e qui est crĂ©Ă©e Ă  l'aide de UnityWebRequestAssetBundle.GetAssetBundle. DownloadHandlerAssetBundle est utilisĂ© pour obtenir des donnĂ©es sous la forme nĂ©cessaire pour 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(); } 

Les principaux problÚmes et solutions lors de l'utilisation d'un serveur Web et de données externes


Des méthodes simples d'interaction entre une application et un serveur en termes de chargement de différentes ressources ont été décrites ci-dessus. Cependant, dans la pratique, les choses sont beaucoup plus compliquées. Considérez les principaux problÚmes qui accompagnent les développeurs et insistez sur les moyens de les résoudre.

Pas assez d'espace libre


L'un des premiers problĂšmes lors du tĂ©lĂ©chargement de donnĂ©es Ă  partir du serveur est un Ă©ventuel manque d'espace libre sur l'appareil. Il arrive souvent que l'utilisateur utilise de vieux appareils pour les jeux (en particulier sur Android), ainsi que la taille des fichiers tĂ©lĂ©chargĂ©s lui-mĂȘme peut ĂȘtre assez grande (bonjour PC). Dans tous les cas, cette situation doit ĂȘtre correctement traitĂ©e et le joueur doit ĂȘtre informĂ© Ă  l'avance qu'il n'y a pas assez d'espace et combien. Comment faire La premiĂšre chose que vous devez savoir est la taille du fichier tĂ©lĂ©chargĂ©, cela se fait au moyen de la demande UnityWebRequest.Head () . Voici le code pour obtenir la taille.

 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); } } 

Il est important de noter une chose ici, pour que la requĂȘte fonctionne correctement, le serveur doit ĂȘtre en mesure de renvoyer la taille du contenu, sinon (comme, en fait, pour afficher la progression), la mauvaise valeur sera retournĂ©e.

AprÚs avoir obtenu la taille des données téléchargées, nous pouvons les comparer avec la taille de l'espace disque libre. Pour obtenir ce dernier, j'utilise le plugin gratuit du Asset Store .

Remarque : vous pouvez utiliser la classe Cache dans Unity3d, elle peut afficher l'espace de cache libre et utilisĂ©. Cependant, il convient de considĂ©rer le fait que ces donnĂ©es sont relatives. Ils sont calculĂ©s en fonction de la taille du cache lui-mĂȘme, par dĂ©faut, il est de 4 Go. Si l'utilisateur a plus d'espace libre que la taille du cache, il n'y aura pas de problĂšme, mais si ce n'est pas le cas, les valeurs peuvent prendre des valeurs incorrectes par rapport Ă  la situation rĂ©elle.

VĂ©rification de l'accĂšs Internet


TrĂšs souvent, avant de tĂ©lĂ©charger quoi que ce soit Ă  partir du serveur, il est nĂ©cessaire de gĂ©rer la situation de manque d'accĂšs Ă  Internet. Il existe plusieurs façons de procĂ©der: du ping d'une adresse Ă  une demande GET sur google.ru. Cependant, Ă  mon avis, le rĂ©sultat le plus correct et le plus rapide et le plus stable consiste Ă  tĂ©lĂ©charger Ă  partir de votre propre serveur (le mĂȘme Ă  partir duquel les fichiers seront tĂ©lĂ©chargĂ©s) un petit fichier. La procĂ©dure Ă  suivre est dĂ©crite ci-dessus dans la section sur l'utilisation du texte.
En plus de vĂ©rifier le fait d'avoir accĂšs Ă  Internet, il est Ă©galement nĂ©cessaire de dĂ©terminer son type (mobile ou WiFi), car il est peu probable qu'un joueur veuille tĂ©lĂ©charger plusieurs centaines de mĂ©gaoctets sur le trafic mobile. Cela peut ĂȘtre fait via la propriĂ©tĂ© Application.internetReachability .

Mise en cache


Le suivant, et l'un des problĂšmes les plus importants, est la mise en cache des fichiers tĂ©lĂ©chargĂ©s. À quoi sert cette mise en cache?

  1. Économisez du trafic (ne tĂ©lĂ©chargez pas les donnĂ©es dĂ©jĂ  tĂ©lĂ©chargĂ©es)
  2. Assurer le travail en l'absence d'Internet (vous pouvez afficher les données du cache).

Qu'est-ce qui doit ĂȘtre mis en cache? La rĂ©ponse Ă  cette question est tout, tous les fichiers que vous tĂ©lĂ©chargez doivent ĂȘtre mis en cache. Comment faire, considĂ©rez ci-dessous et commencez avec des fichiers texte simples.
Malheureusement, Unity n'a pas de mĂ©canisme intĂ©grĂ© pour la mise en cache du texte, ainsi que des textures et des fichiers audio. Par consĂ©quent, pour ces ressources, il est nĂ©cessaire d'Ă©crire votre systĂšme, ou de ne pas Ă©crire, selon les besoins du projet. Dans le cas le plus simple, nous Ă©crivons simplement le fichier dans le cache et, en l'absence d'Internet, nous en prenons le fichier. Dans une version lĂ©gĂšrement plus complexe (je l'utilise dans des projets), nous envoyons une requĂȘte au serveur, qui retourne json indiquant les versions des fichiers qui sont stockĂ©s sur le serveur. Vous pouvez Ă©crire et lire des fichiers Ă  partir du cache Ă  l'aide de la classe C # de la classe File ou de toute autre maniĂšre pratique et acceptĂ©e par votre Ă©quipe.

 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); } 

De mĂȘme, obtenir des donnĂ©es du cache.

 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; } 

Remarque : pourquoi le mĂȘme UWR avec une URL du fichier de formulaire: // n'est pas utilisĂ© pour charger les textures. Pour le moment, il y a des problĂšmes avec cela, le fichier ne se charge tout simplement pas, j'ai donc dĂ» trouver une solution de contournement.

Remarque : Je n'utilise pas le chargement direct d'AudioClip dans les projets, je stocke toutes ces donnĂ©es dans AssetBundle. Cependant, si nĂ©cessaire, cela peut facilement ĂȘtre fait en utilisant les fonctions de la classe AudioClip GetData et SetData.

Les ressources simples d'Unity pour AssetBundle , Unity dispose d'un mécanisme de mise en cache intégré. Examinons-le plus en détail.

Fondamentalement, ce mécanisme peut utiliser deux approches:

  1. Utilisation du CRC et du numéro de version
  2. Utilisation des valeurs de hachage

En principe, vous pouvez utiliser n'importe lequel d'entre eux, mais j'ai dĂ©cidĂ© moi-mĂȘme que Hash est le plus acceptable, car j'ai mon propre systĂšme de version et il prend en compte non seulement la version AssetBundle , mais aussi la version de l'application, car souvent le bundle peut ne pas ĂȘtre compatible avec la version, prĂ©sentĂ© en magasin.

Alors, comment se fait la mise en cache:

  1. Nous demandons un fichier de bundle au serveur manifeste (ce fichier est crĂ©Ă© automatiquement lors de sa crĂ©ation et contient une description des actifs qu'il contient, ainsi que le hachage, le crc, la taille, etc.). Le fichier a le mĂȘme nom que le bundle plus l'extension .manifest.
  2. Obtenez la valeur hash128 du manifeste
  3. Nous crĂ©ons une demande au serveur pour obtenir un AssetBundle, oĂč en plus de l'URL, spĂ©cifiez la valeur de hachage128 reçue

Code pour l'algorithme décrit ci-dessus:
 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(); } 


Dans l'exemple ci-dessus, Unity, lors de l'interrogation du serveur, cherche d'abord à voir s'il y a un fichier dans le cache avec la valeur de hachage 128 spécifiée, si c'est le cas, il sera renvoyé; sinon, le fichier mis à jour sera téléchargé. Pour gérer tous les fichiers de cache dans Unity, il existe une classe de mise en cache , avec laquelle nous pouvons savoir s'il y a un fichier dans le cache, obtenir toutes les versions mises en cache, ainsi que supprimer les versions inutiles ou les effacer complÚtement.

Remarque : pourquoi une telle façon Ă©trange d'obtenir des valeurs de hachage? Cela est dĂ» au fait que l'obtention de hachage128 de la maniĂšre dĂ©crite dans la documentation nĂ©cessite le chargement de l'ensemble complet, puis la rĂ©ception de l'actif AssetBundleManifest de celui-ci et de lĂ  dĂ©jĂ  des valeurs de hachage. L'inconvĂ©nient de cette approche est que l'ensemble du AssetBundle oscille, mais nous avons juste besoin qu'il ne le soit pas. Par consĂ©quent, nous tĂ©lĂ©chargeons d'abord uniquement le fichier manifeste depuis le serveur, prenons hash128 de celui-ci, et seulement ensuite, si nĂ©cessaire, tĂ©lĂ©chargeons le fichier de bundle, et la valeur hash128 doit ĂȘtre extraite via l'interprĂ©tation des lignes.

Travailler avec des ressources en mode Ă©diteur


Le dernier problĂšme, ou plutĂŽt la question du dĂ©bogage et de la commoditĂ© du dĂ©veloppement, est de travailler avec des ressources tĂ©lĂ©chargeables en mode Ă©diteur, s'il n'y a pas de problĂšmes avec les fichiers normaux, alors les choses ne sont pas si simples avec les bundles. Bien sĂ»r, vous pouvez construire leur build Ă  chaque fois, le tĂ©lĂ©charger sur le serveur et lancer l'application dans l'Ă©diteur Unity et regarder comment tout fonctionne, mais mĂȘme cette description ressemble Ă  une "bĂ©quille". Quelque chose doit ĂȘtre fait avec cela et pour cela la classe AssetDatabase nous aidera.

Afin d'unifier le travail avec les bundles, j'ai créé un wrapper spécial:

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

Maintenant, nous devons ajouter deux modes de travail avec les actifs, selon que nous sommes dans l'éditeur ou dans la build. Pour la construction, nous utilisons des wrappers sur les fonctions de la classe AssetBundle , et pour l'éditeur, nous utilisons la classe AssetDatabase mentionnée ci-dessus.

Ainsi, nous obtenons le code suivant:
 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 } 


Remarque : le code utilise la classe TaskManager , il sera discuté ci-dessous, en bref, c'est un wrapper pour travailler avec Coroutine .

En plus de ce qui prĂ©cĂšde, il est Ă©galement utile pendant le dĂ©veloppement de regarder ce que nous avons tĂ©lĂ©chargĂ© et ce qui est actuellement dans le cache. À cette fin, vous pouvez profiter de la possibilitĂ© de dĂ©finir votre propre dossier, qui sera utilisĂ© pour la mise en cache (vous pouvez Ă©galement Ă©crire du texte tĂ©lĂ©chargĂ© et d'autres fichiers dans le mĂȘme dossier):

 #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); 

Nous Ă©crivons un gestionnaire de requĂȘtes rĂ©seau ou travaillons avec un serveur web


Ci-dessus, nous avons examinĂ© les principaux aspects du travail avec des ressources externes dans Unity, maintenant je voudrais m'attarder sur la mise en Ɠuvre de l'API, qui gĂ©nĂ©ralise et unifie tout ce qui prĂ©cĂšde. Et d'abord, attardons-nous sur le gestionnaire de requĂȘtes rĂ©seau.

Remarque : ci-aprĂšs, nous utilisons le wrapper sur Coroutine sous la forme de la classe TaskManager . J'ai Ă©crit sur ce wrapper dans un autre article .

Obtenons la classe correspondante:

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

Le champ statique NetworkType est requis pour que l'application reçoive des informations sur le type de connexion Internet. En principe, cette valeur peut ĂȘtre stockĂ©e n'importe oĂč, j'ai dĂ©cidĂ© que c'Ă©tait la place dans la classe Network .

Ajoutez la fonction de base d'envoi d'une requĂȘte au serveur:
 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(); } 


Comme vous pouvez le voir dans le code, la mĂ©thode de traitement de l'achĂšvement d'une demande a Ă©tĂ© modifiĂ©e par rapport au code des sections prĂ©cĂ©dentes. Il s'agit de montrer la progression du chargement des donnĂ©es. De plus, toutes les demandes envoyĂ©es sont stockĂ©es dans une liste afin que, si nĂ©cessaire, elles puissent ĂȘtre annulĂ©es.

Ajoutez une fonction de crĂ©ation de requĂȘte basĂ©e sur un lien pour 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); } 


De mĂȘme, des fonctions sont crĂ©Ă©es pour la texture, l'audio, le texte, le tableau d'octets.

Vous devez maintenant vous assurer que le serveur envoie des données via la commande Post. Souvent, vous devez transmettre quelque chose au serveur et, en fonction de quoi exactement, obtenir une réponse. Ajoutez les fonctions appropriées.

Envoi de données sous la forme d'un ensemble de valeurs-clés:
 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); } 


Envoi de données au format 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); } 


Nous allons maintenant ajouter des méthodes publiques avec lesquelles nous chargerons des données, en particulier 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); } 


De mĂȘme, des mĂ©thodes sont ajoutĂ©es pour la texture, le fichier audio, le texte, etc.

Et enfin, nous ajoutons la fonction d'obtention de la taille du fichier tĂ©lĂ©chargĂ© et la fonction de nettoyage pour arrĂȘter toutes les requĂȘtes crĂ©Ă©es.
 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(); } 


À ce sujet, notre gestionnaire pour travailler avec les demandes de rĂ©seau est terminĂ©. Si nĂ©cessaire, chaque sous-systĂšme du jeu qui nĂ©cessite de travailler avec le serveur peut crĂ©er ses propres instances de la classe.

Écrire un gestionnaire de chargement de ressources externes


En plus de la classe décrite ci-dessus, pour travailler pleinement avec des données externes, nous avons besoin d'un gestionnaire distinct qui non seulement téléchargera les données, mais informera également l'application du début du chargement, de l'achÚvement, de la progression, du manque d'espace libre et traitera également les problÚmes de mise en cache.

Nous commençons la classe correspondante, qui dans mon cas est 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); } } 


Comme vous pouvez le voir, le concepteur définit le dossier pour la mise en cache, selon que nous soyons dans l'éditeur ou non. De plus, nous avons configuré un champ privé pour une instance de la classe Network, que nous avons décrite précédemment.

Nous allons maintenant ajouter des fonctions auxiliaires pour travailler avec le cache, ainsi que pour déterminer la taille du fichier téléchargé et vérifier l'espace libre pour celui-ci. Plus loin et plus bas, le code est donné sur un exemple de travail avec AssetBundle, pour le reste des ressources tout se fait par analogie.

Code de fonction d'assistance
 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; } 


Ajoutons maintenant des fonctions de chargement de données en utilisant l'exemple 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 } 


Alors, que se passe-t-il dans cette fonction:

  • La directive de prĂ©compilation DONT_USE_SERVER_IN_EDITOR est utilisĂ©e pour dĂ©sactiver le chargement rĂ©el des bundles depuis le serveur.
  • La premiĂšre Ă©tape consiste Ă  faire une demande au serveur pour obtenir le fichier manifeste du bundle
  • - , , - ( _externalResourcesStorage ) , , ( , ), , null
  • , Caching , , ( )
  • , , , , - ( ). , ( )

: -. , , , - , , .

/ : GetJson, GetTexture, GetText, GetAudio ..

, . , , - .
 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; } } } 


Ici, il vaut la peine de comprendre la particularitĂ© du TaskManager , qui est utilisĂ© dans le gestionnaire de requĂȘtes rĂ©seau, par dĂ©faut, il fonctionne, effectuant toutes les tĂąches Ă  tour de rĂŽle. Par consĂ©quent, le tĂ©lĂ©chargement des fichiers se fera en consĂ©quence.

Remarque : pour ceux qui n'aiment pas Coroutine , tout peut ĂȘtre facilement traduit en async / wait , mais dans ce cas, dans l'article, j'ai dĂ©cidĂ© d'utiliser une option plus comprĂ©hensible pour les dĂ©butants (comme il me semble).

Conclusion


. , . , ( f2p ), , , , , .

, :
assetstore.unity.com/packages/tools/simple-disk-utils-59382
habr.com/post/352296
habr.com/post/282524

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


All Articles