1. Introdução
Olá queridos leitores, hoje falaremos sobre como trabalhar com recursos externos no ambiente Unity 3d.
Por tradição, para começar, determinaremos o que é e por que precisamos. Então, quais são exatamente esses recursos externos. Como parte do desenvolvimento do jogo, esses recursos podem ser tudo o que é necessário para o aplicativo funcionar e não devem ser armazenados na compilação final do projeto. Os recursos externos podem estar localizados no disco rígido do computador do usuário e em um servidor da Web externo. No caso geral, esses recursos são qualquer arquivo ou conjunto de dados que carregamos em nosso aplicativo já em execução. Falando no quadro do Unity 3d, eles podem ser:
- Arquivo de texto
- Arquivo de textura
- Arquivo de áudio
- Matriz de bytes
- AssetBundle (arquivo com ativos do projeto Unity 3d)
A seguir, examinaremos mais detalhadamente os mecanismos internos para trabalhar com esses recursos presentes no Unity 3d, além de escrever gerentes simples para interagir com o servidor da Web e carregar recursos no aplicativo.
Nota : o
restante deste artigo usa código usando C # 7+ e foi projetado para o compilador Roslyn usado no Unity3d nas versões 2018.3+.Recursos do Unity 3d
Antes da versão 2017 do Unity, um mecanismo (excluindo a auto-descrição) era usado para trabalhar com dados do servidor e recursos externos, incluídos no mecanismo - esta é a classe WWW. Essa classe permitiu o uso de vários comandos http (get, post, put, etc.) de forma síncrona ou assíncrona (via Coroutine). O trabalho com essa classe foi bastante simples e direto.
IEnumerator LoadFromServer(string url) { var www = new WWW(url); yield return www; Debug.Log(www.text); }
Da mesma forma, você pode obter não apenas dados de texto, mas também outros:
No entanto, a partir da versão 2017, o Unity possui um novo sistema de servidor introduzido pela classe
UnityWebRequest , localizada no namespace Networking. Até o Unity 2018, ele existia junto com a
WWW , mas na versão mais recente do mecanismo da
WWW não era recomendado e, no futuro, será completamente removido. Portanto, focaremos ainda mais o
UnityWebRequest (daqui em diante UWR).
Trabalhar com a UWR como um todo é semelhante à WWW em sua essência, mas há diferenças que serão discutidas mais adiante. Abaixo está um exemplo semelhante de carregamento de texto.
IEnumerator LoadFromServer(string url) { var request = new UnityWebRequest(url); yield return request.SendWebRequest(); Debug.Log(request.downloadHandler.text); request.Dispose(); }
As principais mudanças que o novo sistema UWR introduziu (além de alterar o princípio de trabalhar internamente) é a capacidade de atribuir manipuladores para carregar e baixar dados do próprio servidor; mais detalhes podem ser lidos
aqui . Por padrão, essas são as classes
UploadHandler e
DownloadHandler . O próprio Unity fornece um conjunto de extensões dessas classes para trabalhar com vários dados, como áudio, texturas, ativos etc. Vamos considerar trabalhar com eles com mais detalhes.
Trabalhar com recursos
Text
Trabalhar com texto é uma das opções mais fáceis. O método para baixá-lo já foi descrito. Reescrevemos um pouco com o uso da criação de uma solicitação HTTP Get direta.
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 você pode ver no código, o
DownloadHandler padrão é usado aqui. A propriedade text é um getter que converte uma matriz de bytes em texto codificado em UTF8. O principal uso do carregamento de texto do servidor é receber um arquivo json (representação serializada dos dados em formato de texto). Você pode obter esses dados usando a classe Unity
JsonUtility .
var data = JsonUtility.FromJson<T>(value);
Áudio
Para trabalhar com áudio, você deve usar o método especial de criação da solicitação
UnityWebRequestMultimedia.GetAudioClip e, para obter a representação dos dados no formato necessário para trabalhar no Unity, use
DownloadHandlerAudioClip . Além disso, ao criar uma solicitação, você deve especificar o tipo de dados de áudio representado pela enumeração
AudioType , que define o 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
O download de texturas é semelhante ao dos arquivos de áudio. A solicitação é criada usando
UnityWebRequestTexture.GetTexture . Para obter dados no formato necessário para o Unity, o
DownloadHandlerTexture é usado.
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(); }
Pacote de ativos
Como mencionado anteriormente, o pacote é, de fato, um arquivo com recursos do Unity que podem ser usados em um jogo já em execução. Esses recursos podem ser quaisquer ativos do projeto, incluindo cenas. A exceção são os scripts C #, eles não podem ser transmitidos. Para carregar o
AssetBundle , é usada uma consulta criada usando
UnityWebRequestAssetBundle.GetAssetBundle. DownloadHandlerAssetBundle é usado para obter dados no formato necessário para o 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(); }
Os principais problemas e soluções ao trabalhar com um servidor da Web e dados externos
Métodos simples de interação entre o aplicativo e o servidor em relação ao carregamento de vários recursos foram descritos acima. No entanto, na prática, as coisas são muito mais complicadas. Considere os principais problemas que acompanham os desenvolvedores e pense em maneiras de resolvê-los.
Espaço livre insuficiente
Um dos primeiros problemas ao baixar dados do servidor é uma possível falta de espaço livre no dispositivo. Muitas vezes acontece que o usuário usa dispositivos antigos para jogos (especialmente no Android), assim como o tamanho dos arquivos baixados em si pode ser bastante grande (olá PC). De qualquer forma, essa situação deve ser processada corretamente e o jogador deve ser informado com antecedência de que não há espaço e quantidade suficientes. Como fazer isso? A primeira coisa que você precisa saber é o tamanho do arquivo baixado, isso é feito por meio da solicitação
UnityWebRequest.Head () . Abaixo está o código para obter o tamanho.
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); } }
É importante observar uma coisa aqui: para que a solicitação funcione corretamente, o servidor deve poder retornar o tamanho do conteúdo; caso contrário (como, de fato, para exibir o progresso), o valor errado será retornado.
Depois de obtermos o tamanho dos dados baixados, podemos compará-los com o tamanho do espaço livre em disco. Para obter o último, eu uso o
plugin gratuito da Asset Store .
Nota :
você pode usar a classe Cache no Unity3d, pode mostrar o espaço livre e usado em cache. No entanto, vale considerar o ponto em que esses dados são relativos. Eles são calculados com base no tamanho do próprio cache, por padrão, são 4 GB. Se o usuário tiver mais espaço livre do que o tamanho do cache, não haverá problemas; no entanto, se não for assim, os valores poderão assumir valores incorretos em relação ao estado real das coisas.Verificação de acesso à Internet
Muitas vezes, antes de baixar qualquer coisa do servidor, é necessário lidar com a situação de falta de acesso à Internet. Existem várias maneiras de fazer isso: de um endereço de ping a uma solicitação GET no google.ru. No entanto, na minha opinião, o resultado mais correto e rápido e estável é o download do seu próprio servidor (o mesmo de onde os arquivos serão baixados) um arquivo pequeno. Como fazer isso é descrito acima na seção sobre como trabalhar com texto.
Além de verificar o fato de ter acesso à Internet, também é necessário determinar seu tipo (móvel ou Wi-Fi), porque é improvável que um jogador deseje baixar várias centenas de megabytes no tráfego móvel. Isso pode ser feito através da propriedade
Application.internetReachability .
Armazenamento em cache
A próxima, e uma das questões mais importantes, é o cache dos arquivos baixados. Para que serve esse cache?
- Economize tráfego (não baixe dados já baixados)
- Garantir o trabalho na ausência da Internet (você pode mostrar dados do cache).
O que precisa ser armazenado em cache? A resposta a esta pergunta é tudo, todos os arquivos que você baixa devem ser armazenados em cache. Como fazer isso, considere abaixo e comece com arquivos de texto simples.
Infelizmente, o Unity não possui um mecanismo interno para armazenar em cache texto, bem como texturas e arquivos de áudio. Portanto, para esses recursos, é necessário gravar seu sistema, ou não, dependendo das necessidades do projeto. No caso mais simples, simplesmente escrevemos o arquivo no cache e, na ausência da Internet, retiramos o arquivo. Em uma versão um pouco mais complexa (eu a uso em projetos), enviamos uma solicitação ao servidor, que retorna json indicando as versões dos arquivos armazenados no servidor. Você pode gravar e ler arquivos do cache usando a classe C # da classe
File ou de qualquer outra maneira conveniente e aceita pela sua equipe.
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); }
Da mesma forma, obtendo dados do 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; }
Nota :
por que o mesmo UWR com um URL do arquivo de formulário: // não é usado para carregar texturas. No momento, há problemas com isso, o arquivo simplesmente não carrega, então tive que encontrar uma solução alternativa.Nota :
Eu não uso o carregamento direto do AudioClip em projetos, eu armazeno todos esses dados no AssetBundle. No entanto, se necessário, isso pode ser feito facilmente usando as funções da classe AudioClip GetData e SetData.
Recursos simples do Unity para
AssetBundle , o Unity possui um mecanismo de armazenamento em cache embutido. Vamos considerar com mais detalhes.
Basicamente, esse mecanismo pode usar duas abordagens:
- Usando CRC e número da versão
- Usando valores de hash
Em princípio, você pode usar qualquer um deles, mas decidi por mim mesmo que o Hash é o mais aceitável, pois tenho meu próprio sistema de versões e leva em consideração não apenas a versão do
AssetBundle , mas também a versão do aplicativo, pois geralmente o pacote pode não ser compatível com a versão, apresentado nas lojas.
Então, como é feito o cache:
- Solicitamos um arquivo de pacote configurável ao servidor de manifesto (esse arquivo é criado automaticamente quando é criado e contém uma descrição dos ativos que ele contém, além de hash, crc, tamanho etc.). O arquivo tem o mesmo nome que o pacote configurável mais a extensão .manifest.
- Obter o valor hash128 do manifesto
- Criamos uma solicitação ao servidor para obter um AssetBundle, onde, além da URL, especifica o valor hash128 recebido
Código para o algoritmo descrito acima: IEnumerator LoadAssetBundleFromServerWithCache(string url, Action<AssetBundle> response) {
No exemplo acima, o Unity, mediante solicitação ao servidor, primeiro verifica se existe um arquivo no cache com o valor especificado de hash128; se houver, ele será retornado; caso contrário, o arquivo atualizado será baixado. Para gerenciar todos os arquivos de cache no Unity, existe uma classe
Caching , com a qual podemos descobrir se há um arquivo no cache, obter todas as versões em cache e excluir as desnecessárias ou limpá-las completamente.
Nota :
por que uma maneira tão estranha de obter valores de hash? Isso se deve ao fato de que a obtenção do hash128 da maneira descrita na documentação requer o carregamento de todo o pacote e o recebimento do recurso AssetBundleManifest a partir dele e de lá já existem valores de hash. A desvantagem dessa abordagem é que todo o AssetBundle está oscilando, mas precisamos que não o seja. Portanto, primeiro baixamos apenas o arquivo de manifesto do servidor, extraímos o hash128 e, se necessário, baixamos o arquivo de pacote configurável, e o valor do hash128 deve ser extraído através da interpretação das linhas.Trabalhar com recursos no modo editor
O último problema, ou melhor, a questão da conveniência de depuração e desenvolvimento, é trabalhar com recursos para download no modo de editor; se não houver problemas com arquivos regulares, as coisas não serão tão simples com os pacotes configuráveis. Obviamente, você pode criar a compilação a cada vez, carregá-la no servidor e iniciar o aplicativo no editor do Unity e observar como tudo funciona, mas mesmo essa descrição parece uma "muleta". Algo precisa ser feito com isso e, para isso, a classe
AssetDatabase nos ajudará.
Para unificar o trabalho com pacotes, criei um wrapper especial:
public class AssetBundleWrapper { private readonly AssetBundle _assetBundle; public AssetBundleWrapper(AssetBundle assetBundle) { _assetBundle = assetBundle; } }
Agora, precisamos adicionar dois modos de trabalhar com ativos, dependendo de estarmos no editor ou na compilação. Para a construção, usamos wrappers sobre as funções da classe
AssetBundle e, para o editor, usamos a classe
AssetDatabase mencionada acima.
Assim, obtemos o seguinte 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 : o código usa a classe
TaskManager , será discutido abaixo, em resumo, este é um invólucro para trabalhar com a
Coroutine .
Além do acima, também é útil durante o desenvolvimento verificar o que baixamos e o que está atualmente no cache. Para esse fim, você pode aproveitar a capacidade de definir sua própria pasta, que será usada para armazenar em cache (você também pode gravar o texto baixado e outros arquivos na mesma pasta):
#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);
Escrevemos um gerenciador de solicitações de rede ou trabalhamos com um servidor web
Acima, examinamos os principais aspectos do trabalho com recursos externos no Unity, agora gostaria de me debruçar sobre a implementação da API, que generaliza e unifica todos os itens acima. E primeiro, vamos nos concentrar no gerenciador de consultas da rede.
Nota : a
seguir, usamos o wrapper sobre a Coroutine como uma classe do TaskManager . Eu escrevi sobre esse invólucro em outro artigo .Vamos pegar a classe correspondente:
public class Network { public enum NetworkTypeEnum { None, Mobile, WiFi } public static NetworkTypeEnum NetworkType; private readonly TaskManager _taskManager = new TaskManager(); }
O campo estático
NetworkType é necessário para o aplicativo receber informações sobre o tipo de conexão com a Internet. Em princípio, esse valor pode ser armazenado em qualquer lugar, decidi que é o lugar na classe
Rede .
Adicione a função básica de enviar uma solicitação ao 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 você pode ver no código, o método para processar a conclusão de uma solicitação foi alterado em comparação com o código nas seções anteriores. Isso é para mostrar o progresso do carregamento de dados. Além disso, todas as solicitações enviadas são armazenadas em uma lista para que, se necessário, possam ser canceladas.
Adicione uma função de criação de consulta baseada em link 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); }
Da mesma forma, funções são criadas para textura, áudio, texto e matriz de bytes.
Agora você precisa garantir que o servidor envie dados através do comando Publicar. Muitas vezes, você precisa passar algo para o servidor e, dependendo do que exatamente, obter uma resposta. Adicione as funções apropriadas.
Enviando dados na forma de um conjunto de valores-chave: 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); }
Enviando dados 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); }
Agora vamos adicionar métodos públicos com os quais carregaremos dados, em particular o 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); }
Da mesma forma, métodos são adicionados para textura, arquivo de áudio, texto etc.
E, finalmente, adicionamos a função de obter o tamanho do arquivo baixado e a função de limpeza para interromper todas as solicitações criadas. 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(); }
Nisso, nosso gerente para trabalhar com solicitações de rede está concluído. Se necessário, cada subsistema do jogo que requer trabalho com o servidor pode criar suas próprias instâncias da classe.
Escrevemos o gerente de carregamento de recursos externos
Além da classe descrita acima, para trabalhar totalmente com dados externos, precisamos de um gerente separado que não apenas baixe os dados, mas também notifique o aplicativo sobre o início do carregamento, conclusão, progresso, falta de espaço livre e também lide com problemas de cache.
Começamos a classe correspondente, que no meu caso é um 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 você pode ver, o designer define a pasta para armazenamento em cache, dependendo de estarmos ou não no editor. Além disso, configuramos um campo privado para uma instância da classe Network, que descrevemos anteriormente.Agora, adicionaremos funções auxiliares para trabalhar com o cache, além de determinar o tamanho do arquivo baixado e verificar o espaço livre. Além disso, abaixo, o código é dado em um exemplo de trabalho com o AssetBundle, para o restante dos recursos tudo é feito por analogia.Código da Função 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; }
Agora vamos adicionar funções de carregamento de dados usando o exemplo 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 }
Então, o que acontece nesta função:- A diretiva de pré-compilação DONT_USE_SERVER_IN_EDITOR é usada para desativar o carregamento real de pacotes configuráveis do servidor.
- A primeira etapa é fazer uma solicitação ao servidor para obter o arquivo de manifesto do pacote
- - , , - ( _externalResourcesStorage ) , , ( , ), , null
- , Caching , , ( )
- , , , , - ( ). , ( )
Nota : É importante entender por que o valor do hash é armazenado separadamente. Isso é necessário para o caso em que não há Internet, ou a conexão é instável, ou houve algum tipo de erro de rede e não foi possível baixar o pacote do servidor, neste caso, garantimos o download do pacote do cache, se ele estiver presente .De maneira semelhante ao método descrito acima, no gerenciador, você pode / precisa obter outras funções para trabalhar com dados: GetJson, GetTexture, GetText, GetAudio, etc.E, finalmente, você precisa obter um método que permita baixar conjuntos de recursos. Este método será útil se precisarmos baixar ou atualizar algo no início do aplicativo. 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; } } }
Aqui vale a pena entender a peculiaridade do TaskManager , que é usado no gerenciador de solicitações de rede, por padrão ele funciona, executando todas as tarefas sucessivamente. Portanto, o download dos arquivos ocorrerá de acordo.Nota : para quem não gosta da Coroutine , tudo pode ser facilmente traduzido em assíncrono / aguardar , mas, neste caso, no artigo, decidi usar uma opção mais compreensível para iniciantes (como me parece).Conclusão
Neste artigo, tentei descrever o trabalho com recursos externos de aplicativos de jogos da maneira mais compacta possível. Essa abordagem e código são usados em projetos que foram lançados e estão sendo desenvolvidos com a minha participação. É bastante simples e aplicável em jogos simples, onde não há comunicação constante com o servidor (MMO e outros jogos f2p complexos), mas facilita muito o trabalho se precisarmos baixar materiais adicionais, idiomas, implementar validação de compras no servidor e outros dados que usado uma vez ou não com muita frequência no aplicativo.Links fornecidos no artigo :assetstore.unity.com/packages/tools/simple-disk-utils-59382habr.com/post/352296habr.com/post/282524