Unity提供了DownloadHandlerFile类来进行文件的下载,如果是那种网络比较好的宽带每秒下载速度可以达到20M以上,这样导致IO容易卡住。如果是进游戏前那种提前下载肯定没问题,但是边玩边下这种如果不限制下载速度那么游戏就不会那么流畅了。
Unity提供了DownloadHandlerScript类,开始我以为只要用FileStream自己来写一个比较小长度的Buffer就可以解决问题。如下代码所示,实际测试了一下ReveiveData会在一帧内回调多次导致write操作卡住IO,所以此思路只能作罢。
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下来。最后就是上完整的代码了。
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;
}
}
启动下载调用的代码,这里可以监听下载完成的事件以及错误信息。
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);
}
};
}
下载过程中取消下载
if (GUILayout.Button("<size=200>取消下载 </size>"))
{
downloadHandler?.Dispose();
}
注意如果是下载file://开头的本地文件, 需要在代码中将HttpWebRequest和HttpWebResponse换成FileWebRequest和FileWebResponse其他地方都完全一样。
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本身的已经可以做到同时只下载一个,这个代码就是让同时下载一个的时候能在下载的更慢一些。如果要支持异步下载多个并且限速,就得重新设计了。