在Unity3D中使用外部资源

引言


尊敬的读者您好,今天我们将讨论在Unity 3d环境中使用外部资源的问题。

首先,根据传统,我们将确定它是什么以及为什么需要它。 那么,这些外部资源到底是什么。 作为游戏开发的一部分,此类资源可以是应用程序正常运行所需的所有资源,不应存储在项目的最终版本中。 外部资源既可以位于用户计算机的硬盘上,也可以位于外部Web服务器上。 通常,此类资源是我们加载到已经运行的应用程序中的任何文件或数据集。 在Unity 3d框架中,它们可以是:

  • 文字档
  • 纹理文件
  • 音频文件
  • 字节数组
  • AssetBundle(与Unity 3d项目的资产一起归档)

下面,我们将更详细地研究Unity 3d中存在的用于处理这些资源的内置机制,以及编写用于与Web服务器交互并将资源加载到应用程序中的简单管理器。

注意本文其余部分使用使用C#7+的代码,并且是为2018.3+版本中Unity3d中使用的Roslyn编译器设计的。

Unity 3d的功能


在Unity 2017之前,引擎中包含了一种机制(不包括自我描述的机制)来处理服务器数据和外部资源-这是WWW类。 此类允许以同步或异步形式(通过Coroutine)使用各种http命令(get,post,put等)。 使用此类的工作非常简单明了。

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

同样,您不仅可以获取文本数据,还可以获取其他数据:


但是,从2017版开始,Unity具有UnityWebRequest类引入的新服务器系统,该系统位于Networking名称空间中。 在Unity 2018之前,它与WWW一起存在,但是在WWW引擎的最新版本中,不推荐使用它,并且在将来它将被完全删除。 因此,进一步,我们将仅关注UnityWebRequest (以下简称UWR)。

从整体上来说,使用UWR与WWW的核心相似,但是存在差异,这将在后面讨论。 下面是加载文本的类似示例。

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

新的UWR系统引入的主要更改(除了更改内部工作原理之外)是分配处理程序以从服务器本身上载和下载数据的能力,更多详细信息可以在此处阅读。 默认情况下,这些是类UploadHandlerDownloadHandler 。 Unity本身提供了这些类的扩展集,用于处理各种数据,例如音频,纹理,资产等。 让我们考虑更详细地与他们合作。

处理资源


文字内容


使用文本是最简单的选择之一。 上面已经描述了用于下载它的方法。 我们使用创建直接的http Get请求来重写它。

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

从代码中可以看到,此处使用默认的DownloadHandler 。 text属性是一个将字符串数组转换为UTF8编码文本的getter。 从服务器加载文本的主要用途是接收json文件(文本形式的数据序列化表示)。 您可以使用Unity JsonUtility类获取此数据。

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

音讯


要使用音频,必须使用创建UnityWebRequestMultimedia.GetAudioClip请求的特殊方法,并且要以在Unity中使用所需的形式获取数据表示,必须使用DownloadHandlerAudioClip 。 另外,在创建请求时,您必须指定由AudioType枚举表示的音频数据的类型,该类型设置格式(wav,aiff,oggvorbis等)。

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

质感


下载纹理类似于音频文件。 该请求是使用UnityWebRequestTexture.GetTexture创建的。 为了获得Unity所需格式的数据,使用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(); } 

资产捆绑


如前所述,该捆绑包实际上是具有Unity资源的存档,可以在已经运行的游戏中使用。 这些资源可以是任何项目资产,包括场景。 C#脚本是一个例外;它们无法传递。 要加载AssetBundle ,将使用使用UnityWebRequestAssetBundle.GetAssetBundle创建的查询 DownloadHandlerAssetBundle用于获取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(); } 

使用Web服务器和外部数据时的主要问题和解决方案


上面已经描述了在加载各种资源方面在应用程序和服务器之间进行交互的简单方法。 但是,实际上,事情要复杂得多。 考虑开发人员伴随的主要问题,并详细说明解决问题的方法。

可用空间不足


从服务器下载数据时的第一个问题是设备上可能缺少可用空间。 用户经常使用旧设备进行游戏(尤其是在Android上),并且下载文件本身的大小可能非常大(Hello PC)。 在任何情况下,都必须正确处理此情况,并且必须提前告知播放器没有足够的空间以及多少空间。 怎么做? 您需要知道的第一件事是下载文件的大小,这是通过UnityWebRequest.Head()请求完成的。 下面是获取大小的代码。

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

在此处注意一件事很重要,为了使请求正常工作,服务器必须能够返回内容的大小,否则(实际上,为了显示进度)将返回错误的值。

获得下载数据的大小后,可以将其与可用磁盘空间的大小进行比较。 为了获得后者,我使用了Asset Store中免费插件

注意您可以在Unity3d中使用Cache类,它可以显示可用和已用的缓存空间。 但是,值得考虑这些数据是相对的。 它们是根据缓存本身的大小计算的,默认情况下为4GB。 如果用户的可用空间大于缓存的大小,那么就不会有问题,但是如果不是这样,则这些值可能采用相对于实际事务状态而言不正确的值。

上网检查


通常,在从服务器下载任何内容之前,有必要处理无法访问Internet的情况。 有几种方法可以执行此操作:从ping地址到google.ru的GET请求。 但是,我认为,最正确,最快速,最稳定的结果是从您自己的服务器(文件下载位置相同)下载一个小文件。 上面有关文本的部分中介绍了如何执行此操作。
除了检查是否可以访问Internet之外,还必须确定其类型(移动设备或WiFi),因为播放器不太可能希望下载数百兆的移动流量。 这可以通过Application.internetReachability属性完成。

快取


下一个也是最重要的问题之一是缓存下载的文件。 此缓存用于什么?

  1. 节省流量(不要下载已经下载的数据)
  2. 确保在没有Internet的情况下工作(您可以显示缓存中的数据)。

需要缓存什么? 这个问题的答案就是一切,您下载的所有文件都必须缓存。 如何执行此操作,请在下面考虑,并从简单的文本文件开始。
不幸的是,Unity没有内置的机制来缓存文本,纹理和音频文件。 因此,对于这些资源,有必要根据项目的需要编写系统或不编写系统。 在最简单的情况下,我们只是将文件写入高速缓存,并且在没有Internet的情况下,我们从文件中获取文件。 在稍微复杂一些的版本中(我在项目中使用了它),我们向服务器发送了一个请求,该请求返回json,指示存储在服务器上的文件的版本。 您可以使用File类的C#类或您的团队方便接受的任何其他方式从缓存中写入和读取文件。

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

同样,从缓存中获取数据。

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

注意为什么不使用带有表单文件url的同一UWR // //不加载纹理。 目前,存在问题,该文件根本无法加载,因此我不得不找到一种解决方法。

注意我没有在项目中直接使用AudioClip,而是将所有此类数据存储在AssetBundle中。 但是,如有必要,可以使用AudioClip类GetData和SetData的功能轻松完成此操作。

Unity的AssetBundle的简单资源,Unity具有内置的缓存机制。 让我们更详细地考虑它。

基本上,此机制可以使用两种方法:

  1. 使用CRC和版本号
  2. 使用哈希值

原则上,您可以使用它们中的任何一个,但我自己决定哈希是最可接受的,因为我有自己的版本系统,并且它不仅考虑了AssetBundle版本,还考虑了应用程序的版本,因为捆绑通常可能与该版本不兼容,在商店中展示。

因此,如何完成缓存:

  1. 我们从清单服务器请求一个捆绑文件(此文件在创建时会自动创建,并包含对其包含的资产的描述以及哈希,CRC,大小等)。 该文件与捆绑包加上.manifest扩展名相同。
  2. 从清单中获取hash128值
  3. 我们向服务器创建一个请求以获取AssetBundle,其中除了url外,还指定接收到的hash128值

上述算法的代码:
 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(); } 


在上面的示例中,Unity在查询服务器时,首先查看缓存中是否存在具有指定的hash128值的文件,如果是,则将其返回;否则,将下载更新的文件。 要在Unity中管理所有缓存文件,有一个缓存类,通过该类我们可以找出缓存中是否有文件,获取所有缓存的版本以及删除不必要的版本或完全清除它。

注意为什么使用这种奇怪的方式获取哈希值? 这是由于以下事实:以文档中描述的方式获取hash128要求加载整个bundle,然后再从中以及从那里已经哈希值中接收AssetBundleManifest资产。 这种方法的缺点是整个AssetBundle都在摇摆,但是我们只需要避免这种情况。 因此,我们首先仅从服务器下载清单文件,并从中获取hash128,然后才在必要时下载捆绑文件,并且必须通过对行的解释来提取hash128的值。

在编辑器模式下使用资源


最后一个问题,或者更确切地说是调试和开发便利性的问题,是在编辑器模式下使用可下载的资源,如果常规文件没有问题,那么使用捆绑包就没有那么简单了。 当然,您可以每次构建它们的构建,将其上载到服务器,然后在Unity编辑器中启动应用程序,并观察一切工作原理,但是即使此描述听起来也像“拐杖”。 需要做一些事情, AssetDatabase类将为我们提供帮助。

为了统一使用包,我做了一个特殊的包装:

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

现在,根据我们是在编辑器中还是在构建中,我们需要添加两种使用资产的模式。 对于构建,我们对AssetBundle类的功能使用包装器,对于编辑器,我们使用上述的AssetDatabase类。

因此,我们获得以下代码:
 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 } 


注意 :该代码使用TaskManager类,下面将对其进行讨论,简而言之,这是使用Coroutine的包装器。

除上述内容外,在开发过程中查看我们下载的内容和当前在缓存中的内容也很有用。 为此,您可以利用设置自己的文件夹的功能,该文件夹将用于缓存(您也可以将下载的文本和其他文件写入同一文件夹):

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

我们编写网络请求管理器或使用Web服务器


上面我们检查了在Unity中使用外部资源的主要方面,现在我想详细介绍API的实现,该API概括并统一了以上所有内容。 首先,让我们来谈谈网络查询管理器。

注意此后,我们以TaskManager类的形式在Coroutine上使用包装器。 我在另一篇文章中谈到了这个包装器

让我们获得相应的类:

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

应用程序需要NetworkType静态字段才能接收有关Internet连接类型的信息。 原则上,该值可以存储在任何地方,我决定将其放置在Network类中。

添加向服务器发送请求的基本功能:
 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(); } 


从代码中可以看到,与上一节中的代码相比,处理请求完成的方法已更改。 这是为了显示数据加载的进度。 同样,所有发送的请求都存储在列表中,以便在必要时可以将其取消。

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


同样,为纹理,音频,文本,字节数组创建函数。

现在,您需要确保服务器通过Post命令发送数据。 通常,您需要将某些内容传递给服务器,然后根据确切的内容获得答案。 添加适当的功能。

以键值集的形式发送数据:
 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); } 


将数据作为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); } 


现在,我们将添加用于加载数据的公共方法,尤其是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); } 


同样,添加了纹理,音频文件,文本等方法。

最后,我们添加了获取下载文件大小的功能和清除功能,以停止所有创建的请求。
 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(); } 


至此,我们用于网络请求的经理完成了。 如有必要,需要与服务器一起使用的游戏的每个子系统都可以创建自己的类实例。

我们写加载外部资源的经理


除了上述类之外,为了完全使用外部数据,我们还需要一个单独的管理器,该管理器不仅下载数据,而且还通知应用程序下载开始,完成,进度,可用空间不足以及缓存问题。

我们开始相应的类,在我的情况下是一个单例
 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); } } 


如您所见,设计器将文件夹设置为缓存,具体取决于我们是否在编辑器中。另外,我们为Network类的实例设置了一个私有字段,我们在前面已经介绍过。

现在,我们将添加辅助功能以使用缓存,以及确定下载文件的大小并检查其可用空间。在下面的内容中,将在使用AssetBundle的示例上给出代码,对于其余资源,所有操作均以此类推。

辅助功能代码
 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; } 


现在,使用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 } 


那么此函数会发生什么:

  • 预编译指令DONT_USE_SERVER_IN_EDITOR用于禁用从服务器实际加载捆绑软件。
  • 第一步是向服务器发出请求,以获取捆绑软件的清单文件
  • - , , - ( _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; } } } 


在这里值得了解TaskManager的特殊性,它在网络请求管理器中使用,默认情况下它可以工作,依次执行所有任务。因此,将相应地进行文件下载。

注意:对于那些不喜欢Coroutine的人,可以将所有内容轻松转换为async / await,但是在这种情况下,在本文中,我决定为初学者使用一个更易理解的选项(在我看来)。

结论


在本文中,我试图尽可能紧凑地描述使用游戏应用程序的外部资源。在我的参与下,这些方法和代码用于已发布和正在开发的项目中。它非常简单并且适用于与服务器之间不存在持续通信的简单游戏(MMO和其他复杂的f2p游戏),但是如果我们需要下载其他材料,语言,对购买的服务器端验证以及其他数据进行验证,则可以极大地简化工作。一次或不太经常在应用程序中使用。

引用的参考文献的文章
assetstore.unity.com/packages/tools/simple-disk-utils-59382
habr.com/post/352296
habr.com/post/282524

Source: https://habr.com/ru/post/zh-CN433366/


All Articles