多个UnityWebRequest并行比单个串行更快,程序更复杂。必须弄一个池管理UnityWebRequest,还需要一个AB包状态池,记录每个包是没下载、下载中、下载完成、下载失败。逻辑是:输入一个AB包信息列表,对UnityWebRequest池循环,如果当前是空闲,对AB包状态池循环,如果还没人下载,则给此下载器下载,并且break循环。
我们可以用Update轮询,也可以让下载器下载完通知管理器再分配任务。
在Update里做,先判断AB包是否全部下载完了,就对AB包状态池循环。那么就先对AB包状态池循环,我们发现
如果有AB包没分配下载,就要分配下载;
如果都分配下载了,没下载完,就等待;
如果都是下载完或失败,则返回结果;
发现需要分配下载是最先知道的,全部处理完毕要最后才知道;
然后我们要做成一个模块化的功能,不依赖外部,就输入一个AB包信息列表,最后执行回调,把结果结构体输入,结果包括结果枚举,还有下载失败的AB包清单。
看了一下API,创建UnityWebRequest就要输入url了,那么UnityWebRequest个数和AB包数量一样,下载器开始下载时UnityWebRequest是当场创建的。那么我们维护的是下载器池。
关于下载过程,之前是从开始到结束全部在一个IEnumerator函数里,
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.downloadHandler =
new DownloadHandlerFile(savePath);
var op= request.SendWebRequest();
while (!op.isDone)
{
showBytes?.Invoke(request.downloadedBytes);
yield return null;
}
if (request.result == UnityWebRequest.Result.Success)
{
result.ok = true;
}
else
现在我们改成开始+Update架构,在Update里查看下载器的进度,那么这个UnityWebRequest不能封在using里,否则cpu下载完前都不会离开这个using。
我们在Update里写两个函数,一个是检查是否同时有未下载文件和空闲下载器,有则分配下载,一个是检查每个下载器的状态,如果下载完毕就把UnityWebRequest Dispose,UnityWebRequestAsyncOperation=null。
然后这样有问题,就是我们想每次的下载分配和下载完成都打印,二者一一对应,但是我们在检查下载器状态里打印下载完毕,分配下载时通过UnityWebRequestAsyncOperation.isDone判断下载器空闲,就有下载器没轮到检查状态,就又分配下载了,导致二者对不上。那么我们需要下载器有一个唯一的入口,管理器检查时发现下载完了,设一个标志isDone,分配时下载器要么从没下载过,要么isDone,才能再分配,保证分配、完成打印对上。就是说不是UnityWebRequest下载完了代表下载器就下载完了,还需要管理器检查,打标记。
显示下载的字节数
如果要每帧一更新下载字节数,逻辑有点复杂,因为在下载过程中管理器需要把已下载完成的+每个下载器当前请求已下载的加起来,有文件下载完成需要把这些字节数加入管理器的已下载字节数。
还有一种比较简单的逻辑是一个下载器在下载完成时更新字节数,UI上看起来更新会不频繁一些,遇到大文件半天不更新。但是这样要处理的累加逻辑、要把字节数翻译成M、k为单位的文本工作量会少很多。
然后最终还要保证下载的字节数加起来和预期的相同。
代码
using MyFW;
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
public class ParallelDownloadMgr:
MonoSingletonDontDestroy<ParallelDownloadMgr>
{
List<DownloadAgent> agents;
List<ABDownloadState> ABStates;
public Action<string> debugAction;
[SerializeField]
/// <summary>
/// uint最多表示4G,可能不够
/// </summary>
ulong downloadedByteNum;
public string resFolderUrl;
public string localFolder;
/// <summary>
/// 在一次轮询中已经为某下载器分配了任务
/// </summary>
bool assigned;
bool hasLeft;
DownloadUIInfo[]UIInfos;
public void Init(int downloaderNum)
{
agents=new(downloaderNum);
for (int i = 0; i < downloaderNum; i++)
{
agents.Add(new DownloadAgent((uint)i));
}
}
public void AssignABList(List<ABInfo> list)
{
if(list==null|| list.Count == 0)
{
debugAction?.Invoke("传入的AB列表为空");
return;
}
ABStates = new(list.Count);
for (int i = 0; i < list.Count; i++)
{
ABDownloadState state = new(list[i]);
ABStates.Add(state);
}
hasLeft = true;
}
private void Update()
{
if (ABStates != null && ABStates.Count > 0&&hasLeft)
{
CheckAssign();
foreach (var agent in agents)
{
agent.Update();
}
}
}
void CheckAssign()
{
hasLeft = false;
assigned= false;
foreach (var abState in ABStates)
{
if (abState.downloadState == DownloadState.NotBegin)
{
for(int i = 0;i<agents.Count;i++)
{
var agent = agents[i];
if (agent.IsIdle())
{
debugAction?.Invoke($"为{i}号下载器分配{abState.ABInfo.name}");
agent.Download(abState);
assigned = true;
break;
}
}
}
if (abState.downloadState != DownloadState.OK)
{
hasLeft = true;
}
if (assigned)
{
break;
}
}
if (!hasLeft)
{
debugAction?.Invoke("下载完毕");
ABStates.Clear();
}
}
public void AddDownloadedByteNum(ulong val)
{
downloadedByteNum += val;
}
public DownloadUIInfo[] GetInfo()
{
if (agents == null || agents.Count == 0)
{
return null;
}
if (UIInfos == null)
{
UIInfos = new DownloadUIInfo[agents.Count];
}
for (int i = 0; i < agents.Count; i++)
{
UIInfos[i].Id =(uint)i;
UIInfos[i].progress = agents[i].DownloadedProgress();
UIInfos[i].downloadedByteNum = agents[i].DownloadedByteNum();
}
return UIInfos;
}
}
}
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
namespace MyFarm
{
public class DownloadAgent
{
uint Id;
ABDownloadState _state;
UnityWebRequestAsyncOperation op;
UnityWebRequest request;
public DownloadAgent(uint Id)
{
this.Id = Id;
}
public void Download(ABDownloadState state)
{
if (request != null)
{
request.Dispose();
op=null;
}
_state = state;
_state.downloadState = DownloadState.Downloading;
string fileName = state.ABInfo.name;
string url = $"{ParallelDownloadMgr.Single.resFolderUrl}{fileName}";
string savePath = Path.Combine(Application.persistentDataPath,
ParallelDownloadMgr.Single.localFolder, fileName);
AsyncReturn<byte[]> ret = new AsyncReturn<byte[]>();
request = UnityWebRequest.Get(url);
request.downloadHandler = new DownloadHandlerFile(savePath);
op = request.SendWebRequest();
}
public void Update()
{
if (op != null && op.isDone)
{
Debug.Log($"<color=#FFA500>{Id}下载完了{request.uri}</color>");
ParallelDownloadMgr.Single.AddDownloadedByteNum(request.downloadedBytes);
request.Dispose();
_state.downloadState = DownloadState.OK;
op = null;
}
}
public bool IsIdle()
{
return _state == null||(op==null);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
public class ABDownloadState
{
ABInfo _ABInfo;
public ABInfo ABInfo=>_ABInfo;
public DownloadState downloadState;
public ABDownloadState(ABInfo info)
{
_ABInfo = info;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace MyFarm
{
public enum DownloadState:ushort
{
NotBegin,
Downloading,
OK,
Fail,
}
}
检查器
进度条更新并不灵敏。
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace MyFarm
{
[CustomEditor(typeof(ParallelDownloadMgr))]
public class ParalellDownloadEditor:Editor
{
ParallelDownloadMgr _Mgr;
DownloadUIInfo[] UIInfos;
private void OnEnable()
{
_Mgr = (ParallelDownloadMgr)target;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
UIInfos = _Mgr.GetInfo();
if (UIInfos != null)
{
foreach (var info in UIInfos)
{
EditorGUI.ProgressBar(EditorGUILayout.GetControlRect(false, 20),
info.progress, $"下载器{info.Id}已下载{info.downloadedByteNum}byte");
}
}
}
}
}
namespace MyFarm
{
public struct DownloadUIInfo
{
public uint Id;
public float progress;
public ulong downloadedByteNum;
}
}
本文介绍基于UnityWebRequest实现AB包多线程并行下载的模块化方案,涵盖下载器池与AB包状态池设计、下载任务动态分配机制、下载完成标记同步、字节进度统计与单位转换逻辑,并强调避免using提前释放WebRequest、确保Update中状态检查与任务分配原子性,以及最终校验总下载字节数一致性。

327

被折叠的 条评论
为什么被折叠?



