Unity提供了DownloadHandlerFile类来进行文件的下载,如果是那种网络比较好的宽带每秒下载速度可以达到20M以上,这样导致IO容易卡住。如果是进游戏前那种提前下载肯定没问题,但是边玩边下这种如果不限制下载速度那么游戏就不会那么流畅了。
Unity提供了DownloadHandlerScript类,开始我以为只要用FileStream自己来写一个比较小长度的Buffer就可以解决问题。如下代码所示,实际测试了一下ReveiveData会在一帧内回调多次导致write操作卡住IO,所以此思路只能作罢。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class CustomDownloadHandler : DownloadHandlerScript { FileStream fileStream; private int m_receiveLength = 0; ulong m_ContentLength; public CustomDownloadHandler(byte[] preallocatedBuffer) : base(preallocatedBuffer) { int size = preallocatedBuffer.Length; fileStream = new FileStream(Application.persistentDataPath + "/1.bundle", FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite, size); } protected override bool ReceiveData(byte[] data, int dataLength) { Debug.Log(Time.frameCount + " " + dataLength); //1帧内需要写入大量数据导致IO卡住 m_receiveLength += dataLength; fileStream.Write(data, 0, dataLength); return true; } //....略 } |
既然Unity的API实现不了只能使用C#的API了。我们先达成一个共识,边玩边下同一时刻只能下载一个文件(游戏不卡顿优先,其次才是下载),所以缓冲Buffer可以分配一个静态的。假设最大的下载速度是1M/S 每秒30帧那么每帧Buffer的长度1024/30*1024。
每帧处理的Buffer字节数组已经确定,接着就是要开线程下载了。使用await Task.Run来开线程,它的好处是可以等子线程的下载任务结束在回到主线程,这样就可以把下载完成的事件抛出让逻辑层处理。下载过程中还需要考虑强制断开的问题,可以使用CancellationToken即可。
下载连接建立好以后就开始下载,启动一个while循环,为了避免IO的卡住,这里需要让线程sleep下来。最后就是上完整的代码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
using System; using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using UnityEngine; public class DownloadHandler { public struct Result { public string error; public bool isHttpError => !string.IsNullOrEmpty(error); } static int DEFAULT_SLEEP_TIME = 33; static int DEFAULT_DOWNLOAD_SPEED = 1024; static byte[] DEFAULT_BUFFER = new byte[DEFAULT_DOWNLOAD_SPEED / 30 * 1024]; static int DEFAULT_DOWNLOAD_TIMEOUT = 5; public event Action<Result> completed; public float Progress; public ulong DownloadedBytes; public bool IsDone; private string m_File; private string m_Url; private int m_SleepTime; private Result m_Result; private Stream m_Stream; private FileStream m_FileStream; private HttpWebRequest m_Request; private HttpWebResponse m_Response; private CancellationTokenSource m_Cts; /// <summary> /// 创建下载对象 /// </summary> /// <param name="url">下载路径</param> /// <param name="file">保存路径</param> /// <param name="speed">每秒最大小速度,KB单位</param> public DownloadHandler(string url,string file,int speed) { m_File = file; m_Url = url; m_SleepTime = (int)(DEFAULT_SLEEP_TIME * Mathf.Max(1, (float)DEFAULT_DOWNLOAD_SPEED / speed)); } //开始下载 public void StartDownload() { Download(); } //停止正在下载中的文件 public void Dispose() { m_Cts?.Cancel(); Close(); } async void Download() { m_Cts = new CancellationTokenSource(); CancellationToken token = m_Cts.Token; m_Result = default(Result); DownloadedBytes = 0; IsDone = false; await Task.Run(() => { try { m_Request = (HttpWebRequest)WebRequest.Create(m_Url); m_Response = (HttpWebResponse)m_Request.GetResponse(); long content = m_Response.ContentLength; m_Stream = m_Response.GetResponseStream(); m_Stream.ReadTimeout = DEFAULT_DOWNLOAD_TIMEOUT*1000; m_FileStream = new FileStream(m_File, FileMode.Create, FileAccess.Write, FileShare.ReadWrite, DEFAULT_BUFFER.Length); int read = 0; while (!token.IsCancellationRequested && (read = m_Stream.Read(DEFAULT_BUFFER, 0, DEFAULT_BUFFER.Length)) > 0) { DownloadedBytes += (ulong)read; m_FileStream.Write(DEFAULT_BUFFER, 0, read); Thread.Sleep(m_SleepTime); } } catch (WebException ex) { m_Result.error = ex.ToString(); } finally { Close(); } }, token); try { if (!token.IsCancellationRequested) { IsDone = true; completed?.Invoke(m_Result); } } catch (Exception ex) { Debug.LogError(ex.ToString()); } } void Close() { m_FileStream?.Dispose(); m_Stream?.Dispose(); m_Response?.Dispose(); m_Cts?.Dispose(); m_Cts = null; m_FileStream = null; m_Stream = null; m_Response = null; m_Request = null; } } |
启动下载调用的代码,这里可以监听下载完成的事件以及错误信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
if (GUILayout.Button("<size=200>下载 </size>")) { string url = "https://xxxxxxx.bundle"; string file = Application.persistentDataPath + "/1.bundle"; float t = Time.time; downloadHandler = new DownloadHandler(url, file, 1024);//1024表示每秒下载1M,还可以传512或者256让下载速度继续往下降 downloadHandler.StartDownload(); downloadHandler.completed += (info) => { if (info.isHttpError) { Debug.LogError(info.error); } else { finishTime = Time.time - t; Debug.LogError("fininsh " + finishTime); } }; } |
下载过程中取消下载
1 2 3 4 5 6 |
if (GUILayout.Button("<size=200>取消下载 </size>")) { downloadHandler?.Dispose(); } |
注意如果是下载file://开头的本地文件, 需要在代码中将HttpWebRequest和HttpWebResponse换成FileWebRequest和FileWebResponse其他地方都完全一样。
1 2 3 4 |
m_Request = (HttpWebRequest)WebRequest.Create(m_Url); m_Response = (HttpWebResponse)m_Request.GetResponse(); |
最后在总结一下资源下载。目前根据我们的经验会将下载分成两部分,一部分是启动下载,另一部分是边玩边下。
先说启动下载,它需要尽可能的快,一般这种下载展示就是一个普通的下载进度条,它并不要求高帧率,需要尽最快速度下载完毕。针对这种下载类型可以直接使用unity的DownloadHandlerFile,但是在面对小文件(几K几十K大小)的时候下载速度是非常慢的,因为针对每个文件需要单独建立http的链接,这些都需要额外开销。反而如果是大文件(百M以上大小),每秒下载好几十M都是可以的。
在针对下载小文件慢的问题上其实是可以增加同时下载的数量的,比如同时下载的资源大小不超过一个阀值就继续开下载队列,目前我项目最大开了30个下载队列,动态根据当前下载文件的小灵活变更数量,尽可能保证下载速度足够快。
其次就是边玩边下了,它和启动下载有个本质区别,边玩边下是不能影响用户游戏体验的,如果用户觉得游戏卡住很可能一开始就流失了。也就是说宁可下载的慢也不能下载太快影响操作体验,所以就有了这篇文章的限速。
另外Unity提供的几个下载的类都在这类,核心都是在C++中完成的。
这篇文章起到一个抛砖引玉的过程,欢迎大家一起来讨论,代码是可以直接运行,欢迎大家测试。
- 本文固定链接: https://www.xuanyusong.com/archives/5001
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
那怎么判断玩家的限速是否合理呢,比如一个玩家是千M宽带,一个玩家是小水管
这个主要是解决边玩边下IO影响游戏帧率的问题,如果在细致一些的话可以用高低配分别设置档位
m_SleepTime = (int)(DEFAULT_SLEEP_TIME * Mathf.Max(1, (float)DEFAULT_DOWNLOAD_SPEED / speed));
这里应该是Mathf.Min吧
您好,有一点没有看明白,已经开了一个task下载了,为什么还需要限速呢?
下载的时候会占IO如果不限制下载速度会影响边玩边下的游戏体验。
是移动端带宽有限,不占用过大带宽的意思么
代码有问题,在unity2018.4.0f1下 测试,小文件下载会出现文件不完整的情况,出现 “Failed to decompress data for the AssetBundle ‘xxx’.” 报错
可以具体调试一下吗? 这段代码我们在线上跑过,没有大问题。 只有个小问题就是在同一帧Dispose后又开始启动下载 可能出现线程没跑完的问题。
static byte[] DEFAULT_BUFFER
因为这块的buffer用的是static,所以当同时启动的线程过多,我测试的情况为同时开10个以下测试十几次基本没出现问题,但是超过十个基本必现某些文件和源文件不一致,出现文件损坏的情况;
哦, 这个情况确实是没考虑到, 需求本身就是为了限制下载速度,不太需要同时下载多个。 unity本身的已经可以做到同时只下载一个,这个代码就是让同时下载一个的时候能在下载的更慢一些。如果要支持异步下载多个并且限速,就得重新设计了。