Compare commits

...

89 Commits

Author SHA1 Message Date
何冠峰
3e3661ad95 refactor : 重构代码 2026-01-16 14:37:04 +08:00
何冠峰
611ac886b3 refactor : 重构代码 2026-01-16 10:30:02 +08:00
何冠峰
b0e85017d2 refactor : 重构代码 2026-01-16 10:28:01 +08:00
何冠峰
2b6e7856e7 update mini game 2026-01-14 19:28:30 +08:00
何冠峰
8d461056e4 update extension sample 2026-01-14 19:28:07 +08:00
何冠峰
f188cc715a update test sample 2026-01-14 19:27:39 +08:00
何冠峰
329cae1441 refactor : 重构资源包裹模块 2026-01-14 19:26:40 +08:00
何冠峰
c8c74b8c20 refactor : 重构资源包裹模块 2026-01-13 19:11:49 +08:00
何冠峰
b3d024743c update space shooter 2026-01-13 17:09:18 +08:00
何冠峰
246a62a675 refactor : 重构异步操作模块 2026-01-13 17:01:15 +08:00
何冠峰
354ca5197f refactor : 重构异步操作模块 2026-01-13 14:55:31 +08:00
何冠峰
ce4d6911db refactor : 重构异步操作模块 2026-01-13 12:08:02 +08:00
何冠峰
b796b1a44e refactor : 重构异步操作模块 2026-01-13 11:23:50 +08:00
何冠峰
7198e639d9 refactor : 重构异步操作模块 2026-01-12 16:10:24 +08:00
何冠峰
294fa18fec refactor : 重构异步操作模块 2026-01-12 15:35:00 +08:00
何冠峰
a3f689d815 refactor : 重构异步操作模块 2026-01-12 11:09:27 +08:00
何冠峰
d228e41df7 Merge branch 'dev' of https://github.com/tuyoogame/YooAsset into dev 2026-01-12 10:42:57 +08:00
何冠峰
23032cc269 Merge pull request #718 from absences/desc
add FileSystemParameters description
2026-01-12 10:40:30 +08:00
xiewen
72f02bd73f add FileSystemParameters description 2026-01-09 10:03:57 +08:00
何冠峰
a37663a8c2 refactor : 重构异步操作模块 2026-01-08 19:19:05 +08:00
何冠峰
f375d45bd6 refactor : 重构异步操作模块 2026-01-08 17:22:19 +08:00
何冠峰
3dd3d4ef76 refactor : 重构异步操作模块 2026-01-08 11:21:26 +08:00
何冠峰
f0563cce0b refactor : 重构网络下载模块 2026-01-07 15:52:32 +08:00
何冠峰
9b83dcf723 refactor : 重构网络下载模块 2026-01-07 15:08:05 +08:00
何冠峰
ee67a55c0f refactor : 重构网络下载模块 2026-01-07 10:23:11 +08:00
何冠峰
454afc9ba6 docs : 增加模块的文档说明 2026-01-06 17:12:07 +08:00
何冠峰
539ca3523e style : 规范代码注释 2026-01-06 14:57:15 +08:00
何冠峰
c87efdb509 refactor : 重构网络下载模块
新增通用下载接口,扩展了默认的Unity引擎下载器
2026-01-05 19:44:10 +08:00
何冠峰
1884fab0c2 refactor : remove weak reference handle 2025-12-23 15:07:52 +08:00
何冠峰
e5d0a856a5 perf : 异常处理替换为YOO的异常类 2025-12-05 15:45:04 +08:00
何冠峰
5da8c6baf8 perf : 文件验证和文件下载并发设置为合理的默认值,并限制参数为合理范围。 2025-12-04 21:12:49 +08:00
何冠峰
33356cb270 完善一些高危风险的代码容错机制。 2025-12-04 20:34:29 +08:00
何冠峰
4b6a8ca406 Update CHANGELOG.md 2025-12-04 18:30:51 +08:00
何冠峰
c8e45a6cae Update package.json 2025-12-04 18:30:46 +08:00
何冠峰
1fbc9d26a6 style : 修改注释说明 2025-12-04 18:16:40 +08:00
何冠峰
abb087b02e fix #700 2025-12-03 10:14:55 +08:00
何冠峰
aeaf03011f Merge pull request #694 from fslse/ClearBundleFilesByLocations
ClearBundleFilesByLocations
2025-11-22 09:39:21 +08:00
DESKTOP-FIVME83\Administrator
78d24ad3a6 清理指定地址的文件
(cherry picked from commit 41a1973be8f11c1629b334238546556f54101c35)
2025-11-21 16:29:21 +08:00
何冠峰
aab2c4625e feat #671 2025-11-13 15:54:08 +08:00
何冠峰
079ef75605 feat #682 2025-11-13 11:51:28 +08:00
何冠峰
75881b55f6 fix #683 2025-11-12 18:41:17 +08:00
何冠峰
2020c7d508 Merge pull request #687 from OpenLBE/dev
Localize UI strings to English in editor windows
2025-11-12 09:58:46 +08:00
lark
088d939346 Localize UI strings to English in editor windows
Replaced Chinese UI strings with English equivalents in various editor windows and dialogs, including AssetArtReporter, AssetArtScanner, and AssetBundleBuilder viewers. This improves accessibility for non-Chinese users and standardizes the language across the editor tools.
2025-11-11 14:43:49 +08:00
何冠峰
fa50e91c9f fix #684 2025-11-07 16:37:26 +08:00
何冠峰
2e7b992a4b Merge branch 'dev' of https://github.com/tuyoogame/YooAsset into dev 2025-11-07 16:23:54 +08:00
何冠峰
29fbbd97ff fix #678 2025-11-07 16:23:51 +08:00
何冠峰
abb150e33f Merge pull request #676 from coffeeofnosugar/dev
修复UniTask的扩展包问题
2025-11-04 15:59:20 +08:00
coffee
78e20f434e 修复UniTask的扩展包问题
Unity版本: 2021.3.45f1
yooasset版本:2.3.17
少了一个下划线
2025-11-01 23:24:03 +08:00
何冠峰
304222b788 Update EditorDefine.cs 2025-10-31 17:03:27 +08:00
何冠峰
101236db8a Update CHANGELOG.md 2025-10-30 20:49:54 +08:00
何冠峰
37de007b3f Update package.json 2025-10-30 20:49:51 +08:00
何冠峰
d570ba8d74 fix #661 2025-10-30 18:31:06 +08:00
何冠峰
2a956099ae 移除程序集里冗余引用 2025-10-30 11:25:21 +08:00
何冠峰
74037a5a29 fix #670 2025-10-28 19:16:51 +08:00
何冠峰
861f850a32 Merge branch 'dev' of https://github.com/tuyoogame/YooAsset into dev 2025-10-27 17:42:32 +08:00
何冠峰
8b1b5f988a fix #669 2025-10-27 17:42:29 +08:00
何冠峰
efafd1173f Merge pull request #667 from yueh0607/fix/chinese-input-textfield
Fix Chinese input issue in TextField by enabling isDelayed
2025-10-27 09:50:52 +08:00
yzp
03e49ff1fb Fix Chinese input issue in TextField by enabling isDelayed
Fixed an issue where Chinese IME candidate characters were being incorrectly inserted into TextFields (PackageDesc, GroupDesc, etc.), causing garbled text like "默认包baobabmo'rmom".

The fix sets `isDelayed = true` on all TextFields in AssetBundleCollectorWindow, which defers value change callbacks until the user completes input (by pressing Enter or losing focus), thus avoiding interference from IME candidate characters.

Modified TextFields:
- PackageName
- PackageDesc
- GroupName
- GroupDesc
- GroupTags
- User Data (collector)
- Asset Tags (collector)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 00:01:48 +08:00
何冠峰
ea34be1f00 fix #646 2025-10-15 17:27:08 +08:00
何冠峰
831a9981e3 Update SearchCacheFilesOperation.cs 2025-10-14 16:58:06 +08:00
何冠峰
9de4aaa658 Merge pull request #656 from AlanLiu90/dev
优化SearchCacheFilesOperation
2025-10-14 16:43:07 +08:00
Alan Liu
3579a23bd5 优化SearchCacheFilesOperation 2025-10-13 18:55:49 +08:00
何冠峰
c865ddc7f2 feat #650 2025-10-10 10:20:02 +08:00
何冠峰
0bde506aec feat #648 2025-10-09 19:10:26 +08:00
何冠峰
15005b3d30 sample : 程序集宏支持
YOO_MACRO_SUPPORT开启扩展代码
2025-10-09 19:07:03 +08:00
何冠峰
4caf733ac6 fix #652 2025-10-09 16:17:44 +08:00
何冠峰
014b17f5cb feat #643 2025-10-09 16:14:31 +08:00
何冠峰
f3ebda0c04 refactor : 重构资源清单反序列化逻辑 2025-09-30 15:35:32 +08:00
何冠峰
5602addaca fix #645
修复极端情况下Shader变种收集不完整的问题
2025-09-24 16:37:46 +08:00
何冠峰
c4ae67aa8e perf: 资源扫描不再主动生成报告文件 2025-09-23 11:08:55 +08:00
何冠峰
bbcc3bf971 style : 修改代码注释说明 2025-09-19 10:05:45 +08:00
何冠峰
8b0e75b9b3 feat : 扩展Taptap小游戏文件类 2025-09-19 10:05:16 +08:00
何冠峰
b1f02049cc Update CHANGELOG.md 2025-09-17 16:59:24 +08:00
何冠峰
512886cdf6 Update package.json 2025-09-17 16:58:59 +08:00
何冠峰
0c3ccc5c2f refactor: 适配引擎版本 2025-09-17 16:49:21 +08:00
何冠峰
b0ea03170f refactor : 移除下载器的timeout参数
可以使用看门狗机制代替。
2025-09-17 15:53:22 +08:00
何冠峰
2c3b890329 feat #642 2025-09-17 15:39:47 +08:00
何冠峰
0f39cb9444 update space shooter 2025-09-17 10:53:56 +08:00
何冠峰
8acc16d3f6 perf #632 2025-09-17 10:53:40 +08:00
何冠峰
fd1715a89b fix 修正无效警告 2025-09-17 10:51:24 +08:00
何冠峰
0934c813d1 fix #644 2025-09-13 18:18:31 +08:00
何冠峰
be71d38cd8 update AssetBundleBuilder 2025-09-11 19:20:16 +08:00
何冠峰
4cdfde31da update AssetArtScanner 2025-09-11 19:19:55 +08:00
何冠峰
0133549ef8 feat #638
修复问题,防止游离的Bundle加载任务出现。
2025-09-11 14:03:42 +08:00
何冠峰
81d9eb47c8 feat #638 2025-09-11 11:07:13 +08:00
何冠峰
5fde689f1f feat #640 2025-09-10 21:19:58 +08:00
何冠峰
5d51bfe751 feat #639
修复问题
2025-09-10 21:00:51 +08:00
何冠峰
ed86edd2b0 feat #639 2025-09-10 20:15:52 +08:00
何冠峰
f627b5b59a Update CHANGELOG.md 2025-09-10 12:05:31 +08:00
355 changed files with 16424 additions and 5113 deletions

View File

@@ -2,6 +2,197 @@
All notable changes to this package will be documented in this file.
## [2.3.18] - 2025-12-04
### Fixed
- (#676) 修复了UniTask扩展包的编译报错。
- (#684) 修复了资源配置窗口Group列表数量过多的时候添加和删除按钮会变小的问题。
- (#700) [**严重**] 修复了小游戏扩展库的下载器再失败后重试逻辑不起效的问题。
### Added
- (#683) 新增了内置文件系统类初始化参数UNPACK_FILE_SYSTEM_ROOT
```csharp
class FileSystemParametersDefine
{
// 指定解压文件的根目录
public const string UNPACK_FILE_SYSTEM_ROOT = "UNPACK_FILE_SYSTEM_ROOT";
}
```
- (#682) 原生文件构建管线新增构建参数IncludePathInHash
```csharp
class RawFileBuildParameters : BuildParameters
{
/// <summary>
/// 文件哈希值计算包含路径信息
/// </summary>
public bool IncludePathInHash = false;
}
```
- (#671) 新增扩展工具,可以生成空的包裹内置资源目录文件。
```csharp
public class CreateEmptyCatalogWindow : EditorWindow
```
- (#694) 新增资源清理方式ClearBundleFilesByLocations
```csharp
public enum EFileClearMode
{
/// <summary>
/// 清理指定地址的文件
/// 说明需要指定参数可选string, string[], List<string>
/// </summary>
ClearBundleFilesByLocations,
}
```
## [2.3.17] - 2025-10-30
**非常重要**:修复了#627优化导致的资源清单CRC值为空的问题。
该问题会导致下载的损坏文件验证通过。
影响范围v2.3.15版本v2.3.16版本。
**非常重要**(#661) 修复了Package销毁过程中遇到正在加载的AssetBundle会导致无法卸载的问题。
该问题是偶现引擎会提示AssetBundle已经加载无法加载新的文件导致资源对象加载失败
影响范围:所有版本!
### Fixed
- (#645) 修复了着色器变种收集工具,在极端情况下变种收集不完整的问题。
- (#646) 修复了EditorSimulateMode模式下开启模拟下载tag不生效的问题。
- (#667) 修复了所有编辑器窗口针对中文IME的输入问题。
- (#670) 修复了Catalog文件生成过程中白名单未考虑自定义清单前缀名。
### Improvements
- 重构并统一了资源清单的反序列化逻辑。
- (#650) 解决互相依赖的资源包无法卸载的问题。需要开启宏定义YOOASSET_EXPERIMENTAL
- (#655) 优化了初始化的时候缓存文件搜索效率。安卓平台性能提升1倍IOS平台性能提升3倍。
### Added
- (#643) 新增构建参数,可以节省资源清单运行时内存
```csharp
class ScriptableBuildParameters
{
/// <summary>
/// 使用可寻址地址代替资源路径
/// 说明:开启此项可以节省运行时清单占用的内存!
/// </summary>
public bool ReplaceAssetPathWithAddress = false;
}
```
- (#648) 新增初始化参数,可以自动释放引用计数为零的资源包
```csharp
class InitializeParameters
{
/// <summary>
/// 当资源引用计数为零的时候自动释放资源包
/// </summary>
public bool AutoUnloadBundleWhenUnused = false;
}
```
### Changed
- 程序集宏定义代码转移到扩展工程。参考MacroSupport文件夹。
## [2.3.16] - 2025-09-17
### Improvements
- (#638) 优化了Provider加载机制引用计数为零时自动挂起
### Fixed
- (#644) [**严重**] 修复了2.3.15版本,资产量巨大的情况下,编辑器下模拟模式初始化耗时很久的问题。
### Added
- (#639) 新增了文件系统参数VIRTUAL_DOWNLOAD_MODE 和 VIRTUAL_DOWNLOAD_SPEED
编辑器下不需要构建AB也可以模拟远端资源下载等同真机运行环境。
```csharp
class DefaultEditorFIleSystem
{
/// <summary>
/// 模拟虚拟下载模式
/// </summary>
public bool VirtualDownloadMode { private set; get; } = false;
/// <summary>
/// 模拟虚拟下载的网速(单位:字节)
/// </summary>
public int VirtualDownloadSpeed { private set; get; } = 1024;
}
```
- (#640) 新增了文件系统参数VIRTUAL_WEBGL_MODE
编辑器下不需要构建AB也可以模拟小游戏开发环境等同真机运行环境。
```csharp
class DefaultEditorFIleSystem
{
/// <summary>
/// 模拟WebGL平台模式
/// </summary>
public bool VirtualWebGLMode { private set; get; } = false;
}
```
- (#642) 新增了文件系统参数DOWNLOAD_WATCH_DOG_TIME
监控时间范围内,如果没有接收到任何下载数据,那么直接终止任务!
```csharp
class DefaultCacheFIleSystem
{
/// <summary>
/// 自定义参数:下载任务的看门狗机制监控时间
/// </summary>
public int DownloadWatchDogTime { private set; get; } = int.MaxValue;
}
```
### Changed
- 下载器参数timeout移除。
可以使用文件系统的看门狗机制代替。
- (#632) IFilterRule接口变动。
收集器可以指定搜寻的资源类型,在收集目录资产量巨大的情况下,可以极大加快打包速度!
```csharp
public interface IFilterRule
{
/// <summary>
/// 搜寻的资源类型
/// 说明:使用引擎方法搜索获取所有资源列表
/// </summary>
string FindAssetType { get; }
}
```
## [2.3.15] - 2025-09-09
**重要**:升级了资源清单版本,不兼容老版本。建议重新提审安装包。
@@ -69,7 +260,9 @@ All notable changes to this package will be documented in this file.
- (#617) 新增资源收集配置参数SupportExtensionless
在不需要模糊加载模式的前提下,开启此选项,可以降低运行时内存大小。
在不需要模糊加载模式的前提下,关闭此选项,可以降低运行时内存大小。
该选项默认开启!
```csharp
public class CollectCommand

View File

@@ -261,14 +261,14 @@ namespace YooAsset.Editor
catch (System.Exception e)
{
_reportCombiner = null;
_titleLabel.text = "导入报告失败!";
_titleLabel.text = "Failed to import report!";
_descLabel.text = e.Message;
UnityEngine.Debug.LogError(e.StackTrace);
}
}
private void FixAllBtn_clicked()
{
if (EditorUtility.DisplayDialog("提示", "修复全部资源(排除白名单和隐藏元素)", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", "Fix all resources (excluding whitelist and hidden elements)", "Yes", "No"))
{
if (_reportCombiner != null)
_reportCombiner.FixAll();
@@ -276,7 +276,7 @@ namespace YooAsset.Editor
}
private void FixSelectBtn_clicked()
{
if (EditorUtility.DisplayDialog("提示", "修复勾选资源(包含白名单和隐藏元素)", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", "Fix selected resources (including whitelist and hidden elements)", "Yes", "No"))
{
if (_reportCombiner != null)
_reportCombiner.FixSelect();
@@ -302,7 +302,7 @@ namespace YooAsset.Editor
}
private void ExportFilesBtn_clicked()
{
string selectFolderPath = EditorUtility.OpenFolderPanel("导入所有选中资源", EditorTools.GetProjectPath(), string.Empty);
string selectFolderPath = EditorUtility.OpenFolderPanel("Export all selected resources", EditorTools.GetProjectPath(), string.Empty);
if (string.IsNullOrEmpty(selectFolderPath) == false)
{
if (_reportCombiner != null)

View File

@@ -32,21 +32,14 @@ namespace YooAsset.Editor
// 开始扫描工作
ScanReport report = scanner.RunScanner();
// 检测报告合法性
report.CheckError();
// 保存扫描结果
string saveDirectory = scanner.SaveDirectory;
if (string.IsNullOrEmpty(saveDirectory))
saveDirectory = "Assets/";
string filePath = $"{saveDirectory}/{scanner.ScannerName}_{scanner.ScannerDesc}.json";
ScanReportConfig.ExportJsonConfig(filePath, report);
return new ScannerResult(filePath, report);
// 返回扫描结果
return new ScannerResult(report);
}
catch (Exception e)
{
return new ScannerResult(e.StackTrace);
return new ScannerResult(e.Message, e.StackTrace);
}
}

View File

@@ -240,7 +240,7 @@ namespace YooAsset.Editor
}
private void ScanAllBtn_clicked()
{
if (EditorUtility.DisplayDialog("提示", $"开始全面扫描!", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", $"Start full scan!", "Yes", "No"))
{
string searchKeyWord = _scannerSearchField.value;
AssetArtScannerSettingData.ScanAll(searchKeyWord);
@@ -248,7 +248,7 @@ namespace YooAsset.Editor
}
else
{
Debug.LogWarning("全面扫描已经取消");
Debug.LogWarning("Full scan has been canceled.");
}
}
private void ScanBtn_clicked()

View File

@@ -3,11 +3,6 @@ namespace YooAsset.Editor
{
public class ScannerResult
{
/// <summary>
/// 生成的报告文件路径
/// </summary>
public string ReprotFilePath { private set; get; }
/// <summary>
/// 报告对象
/// </summary>
@@ -18,6 +13,11 @@ namespace YooAsset.Editor
/// </summary>
public string ErrorInfo { private set; get; }
/// <summary>
/// 错误堆栈
/// </summary>
public string ErrorStack { private set; get; }
/// <summary>
/// 是否成功
/// </summary>
@@ -33,15 +33,14 @@ namespace YooAsset.Editor
}
public ScannerResult(string error)
public ScannerResult(string error, string stack)
{
ErrorInfo = error;
ErrorStack = stack;
}
public ScannerResult(string filePath, ScanReport report)
public ScannerResult(ScanReport report)
{
ReprotFilePath = filePath;
Report = report;
ErrorInfo = string.Empty;
}
/// <summary>
@@ -55,5 +54,19 @@ namespace YooAsset.Editor
reproterWindow.ImportSingleReprotFile(Report);
}
}
/// <summary>
/// 保存报告文件
/// </summary>
public void SaveReportFile(string saveDirectory)
{
if (Report == null)
throw new System.Exception("Scan report is invalid !");
if (string.IsNullOrEmpty(saveDirectory))
saveDirectory = "Assets/";
string filePath = $"{saveDirectory}/{Report.ReportName}_{Report.ReportDesc}.json";
ScanReportConfig.ExportJsonConfig(filePath, Report);
}
}
}

View File

@@ -19,7 +19,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = EBuildPipeline.EditorSimulateBuildPipeline.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.VirtualBundle;
buildParameters.BuildBundleType = (int)EBundleType.VirtualBundle;
buildParameters.BuildTarget = EditorUserBuildSettings.activeBuildTarget;
buildParameters.PackageName = packageName;
buildParameters.PackageVersion = "Simulate";

View File

@@ -106,7 +106,27 @@ namespace YooAsset.Editor
/// </summary>
public string[] GetAllPackAssetPaths()
{
return AllPackAssets.Select(t => t.AssetInfo.AssetPath).ToArray();
List<string> results = new List<string>(AllPackAssets.Count);
for (int i = 0; i < AllPackAssets.Count; i++)
{
var packAsset = AllPackAssets[i];
results.Add(packAsset.AssetInfo.AssetPath);
}
return results.ToArray();
}
/// <summary>
/// 获取构建的资源可寻址列表
/// </summary>
public string[] GetAllPackAssetAddress()
{
List<string> results = new List<string>(AllPackAssets.Count);
for (int i = 0; i < AllPackAssets.Count; i++)
{
var packAsset = AllPackAssets[i];
results.Add(packAsset.Address);
}
return results.ToArray();
}
/// <summary>
@@ -153,13 +173,15 @@ namespace YooAsset.Editor
/// <summary>
/// 创建AssetBundleBuild类
/// </summary>
public UnityEditor.AssetBundleBuild CreatePipelineBuild()
public UnityEditor.AssetBundleBuild CreatePipelineBuild(bool replaceAssetPathWithAddress)
{
// 注意我们不再支持AssetBundle的变种机制
AssetBundleBuild build = new AssetBundleBuild();
build.assetBundleName = BundleName;
build.assetBundleVariant = string.Empty;
build.assetNames = GetAllPackAssetPaths();
if (replaceAssetPathWithAddress)
build.addressableNames = GetAllPackAssetAddress();
return build;
}

View File

@@ -96,12 +96,12 @@ namespace YooAsset.Editor
/// <summary>
/// 获取构建管线里需要的数据
/// </summary>
public UnityEditor.AssetBundleBuild[] GetPipelineBuilds()
public UnityEditor.AssetBundleBuild[] GetPipelineBuilds(bool replaceAssetPathWithAddres)
{
List<UnityEditor.AssetBundleBuild> builds = new List<UnityEditor.AssetBundleBuild>(_bundleInfoDic.Count);
foreach (var bundleInfo in _bundleInfoDic.Values)
{
builds.Add(bundleInfo.CreatePipelineBuild());
builds.Add(bundleInfo.CreatePipelineBuild(replaceAssetPathWithAddres));
}
return builds.ToArray();
}

View File

@@ -96,7 +96,7 @@ namespace YooAsset.Editor
/// <summary>
/// 资源包加密服务类
/// </summary>
public IEncryptionServices EncryptionServices;
public IBundleEncryptionServices EncryptionServices;
/// <summary>
/// 资源清单加密服务类
@@ -146,7 +146,7 @@ namespace YooAsset.Editor
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildPipelineIsNullOrEmpty, "Build pipeline is null or empty !");
throw new Exception(message);
}
if (BuildBundleType == (int)EBuildBundleType.Unknown)
if (BuildBundleType == (int)EBundleType.Unknown)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.BuildBundleTypeIsUnknown, $"Build bundle type is unknown {BuildBundleType} !");
throw new Exception(message);

View File

@@ -15,7 +15,7 @@ namespace YooAsset.Editor
string buildinRootDirectory = buildParametersContext.GetBuildinRootDirectory();
string buildPackageName = buildParametersContext.Parameters.PackageName;
var manifestServices = buildParametersContext.Parameters.ManifestRestoreServices;
CatalogTools.CreateCatalogFile(manifestServices, buildPackageName, buildinRootDirectory);
CatalogFileHelper.CreateFile(manifestServices, buildPackageName, buildinRootDirectory);
// 刷新目录
AssetDatabase.Refresh();

View File

@@ -19,7 +19,7 @@ namespace YooAsset.Editor
/// <summary>
/// 创建补丁清单文件到输出目录
/// </summary>
protected void CreateManifestFile(bool processBundleDepends, bool processBundleTags, BuildContext context)
protected void CreateManifestFile(bool processBundleDepends, bool processBundleTags, bool replaceAssetPathWithAddress, BuildContext context)
{
var buildMapContext = context.GetContextObject<BuildMapContext>();
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
@@ -31,11 +31,12 @@ namespace YooAsset.Editor
// 创建新补丁清单
PackageManifest manifest = new PackageManifest();
manifest.FileVersion = ManifestDefine.FileVersion;
manifest.FileVersion = PackageManifestDefine.FileVersion;
manifest.EnableAddressable = buildMapContext.Command.EnableAddressable;
manifest.SupportExtensionless = buildMapContext.Command.SupportExtensionless;
manifest.LocationToLower = buildMapContext.Command.LocationToLower;
manifest.IncludeAssetGUID = buildMapContext.Command.IncludeAssetGUID;
manifest.ReplaceAssetPathWithAddress = replaceAssetPathWithAddress;
manifest.OutputNameStyle = (int)buildParameters.FileNameStyle;
manifest.BuildBundleType = buildParameters.BuildBundleType;
manifest.BuildPipeline = buildParameters.BuildPipeline;
@@ -58,13 +59,19 @@ namespace YooAsset.Editor
// 4. 处理内置资源包
if (processBundleDepends)
{
// 注意:初始化资源清单建立引用关系
manifest.Initialize();
ProcessBuiltinBundleDependency(context, manifest);
}
// 创建资源清单文本文件
{
string fileName = YooAssetSettingsData.GetManifestJsonFileName(buildParameters.PackageName, buildParameters.PackageVersion);
string filePath = $"{packageOutputDirectory}/{fileName}";
ManifestTools.SerializeToJson(filePath, manifest);
PackageManifestTools.SerializeToJson(filePath, manifest);
BuildLogger.Log($"Create package manifest file: {filePath}");
}
@@ -74,7 +81,7 @@ namespace YooAsset.Editor
{
string fileName = YooAssetSettingsData.GetManifestBinaryFileName(buildParameters.PackageName, buildParameters.PackageVersion);
packagePath = $"{packageOutputDirectory}/{fileName}";
ManifestTools.SerializeToBinary(packagePath, manifest, buildParameters.ManifestProcessServices);
PackageManifestTools.SerializeToBinary(packagePath, manifest, buildParameters.ManifestProcessServices);
packageHash = HashUtility.FileCRC32(packagePath);
BuildLogger.Log($"Create package manifest file: {packagePath}");
}
@@ -99,7 +106,7 @@ namespace YooAsset.Editor
{
ManifestContext manifestContext = new ManifestContext();
byte[] bytesData = FileUtility.ReadAllBytes(packagePath);
manifestContext.Manifest = ManifestTools.DeserializeFromBinary(bytesData, buildParameters.ManifestRestoreServices);
manifestContext.Manifest = PackageManifestTools.DeserializeFromBinary(bytesData, buildParameters.ManifestRestoreServices);
context.SetContextObject(manifestContext);
}
}
@@ -302,9 +309,6 @@ namespace YooAsset.Editor
#region YOOASSET_LEGACY_DEPENDENCY
private void ProcessBuiltinBundleDependency(BuildContext context, PackageManifest manifest)
{
// 注意:初始化资源清单建立引用关系
ManifestTools.InitManifest(manifest);
// 注意:如果是可编程构建管线,需要补充内置资源包
// 注意:该步骤依赖前面的操作!
var buildResultContext = context.TryGetContextObject<TaskBuilding_SBP.BuildResultContext>();

View File

@@ -54,6 +54,7 @@ namespace YooAsset.Editor
buildReport.Summary.CompressOption = builtinBuildParameters.CompressOption;
buildReport.Summary.DisableWriteTypeTree = builtinBuildParameters.DisableWriteTypeTree;
buildReport.Summary.IgnoreTypeTreeChanges = builtinBuildParameters.IgnoreTypeTreeChanges;
buildReport.Summary.ReplaceAssetPathWithAddress = builtinBuildParameters.ReplaceAssetPathWithAddress;
}
else if (buildParameters is ScriptableBuildParameters)
{
@@ -61,6 +62,7 @@ namespace YooAsset.Editor
buildReport.Summary.CompressOption = scriptableBuildParameters.CompressOption;
buildReport.Summary.DisableWriteTypeTree = scriptableBuildParameters.DisableWriteTypeTree;
buildReport.Summary.IgnoreTypeTreeChanges = scriptableBuildParameters.IgnoreTypeTreeChanges;
buildReport.Summary.ReplaceAssetPathWithAddress = scriptableBuildParameters.ReplaceAssetPathWithAddress;
buildReport.Summary.WriteLinkXML = scriptableBuildParameters.WriteLinkXML;
buildReport.Summary.CacheServerHost = scriptableBuildParameters.CacheServerHost;
buildReport.Summary.CacheServerPort = scriptableBuildParameters.CacheServerPort;

View File

@@ -24,7 +24,7 @@ namespace YooAsset.Editor
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
foreach (var bundleInfo in buildMapContext.Collection)
{
EncryptFileInfo fileInfo = new EncryptFileInfo();
EncryptBundleInfo fileInfo = new EncryptBundleInfo();
fileInfo.BundleName = bundleInfo.BundleName;
fileInfo.FileLoadPath = $"{pipelineOutputDirectory}/{bundleInfo.BundleName}";
var encryptResult = encryptionServices.Encrypt(fileInfo);

View File

@@ -44,9 +44,9 @@ namespace YooAsset.Editor
{
bundleInfo.PackageUnityHash = GetUnityHash(bundleInfo, context);
bundleInfo.PackageUnityCRC = GetUnityCRC(bundleInfo, context);
bundleInfo.PackageFileHash = GetBundleFileHash(bundleInfo, buildParametersContext);
bundleInfo.PackageFileCRC = GetBundleFileCRC(bundleInfo, buildParametersContext);
bundleInfo.PackageFileSize = GetBundleFileSize(bundleInfo, buildParametersContext);
bundleInfo.PackageFileHash = GetBundleFileHash(bundleInfo, context);
bundleInfo.PackageFileCRC = GetBundleFileCRC(bundleInfo, context);
bundleInfo.PackageFileSize = GetBundleFileSize(bundleInfo, context);
}
// 4.更新补丁包输出的文件路径
@@ -54,16 +54,16 @@ namespace YooAsset.Editor
{
string bundleName = bundleInfo.BundleName;
string fileHash = bundleInfo.PackageFileHash;
string fileExtension = ManifestTools.GetRemoteBundleFileExtension(bundleName);
string fileName = ManifestTools.GetRemoteBundleFileName(outputNameStyle, bundleName, fileExtension, fileHash);
string fileExtension = PackageManifestTools.GetRemoteBundleFileExtension(bundleName);
string fileName = PackageManifestTools.GetRemoteBundleFileName(outputNameStyle, bundleName, fileExtension, fileHash);
bundleInfo.PackageDestFilePath = $"{packageOutputDirectory}/{fileName}";
}
}
protected abstract string GetUnityHash(BuildBundleInfo bundleInfo, BuildContext context);
protected abstract uint GetUnityCRC(BuildBundleInfo bundleInfo, BuildContext context);
protected abstract string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext);
protected abstract uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext);
protected abstract long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext);
protected abstract string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context);
protected abstract uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context);
protected abstract long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context);
}
}

View File

@@ -23,7 +23,8 @@ namespace YooAsset.Editor
// 开始构建
string pipelineOutputDirectory = buildParametersContext.GetPipelineOutputDirectory();
BuildAssetBundleOptions buildOptions = builtinBuildParameters.GetBundleBuildOptions();
AssetBundleManifest unityManifest = BuildPipeline.BuildAssetBundles(pipelineOutputDirectory, buildMapContext.GetPipelineBuilds(), buildOptions, buildParametersContext.Parameters.BuildTarget);
var bundleBuilds = buildMapContext.GetPipelineBuilds(builtinBuildParameters.ReplaceAssetPathWithAddress);
AssetBundleManifest unityManifest = BuildPipeline.BuildAssetBundles(pipelineOutputDirectory, bundleBuilds, buildOptions, buildParametersContext.Parameters.BuildTarget);
if (unityManifest == null)
{
string message = BuildLogger.GetErrorMessage(ErrorCode.UnityEngineBuildFailed, "UnityEngine build failed !");

View File

@@ -11,7 +11,10 @@ namespace YooAsset.Editor
void IBuildTask.Run(BuildContext context)
{
CreateManifestFile(true, true, context);
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var builtinBuildParameters = buildParametersContext.Parameters as BuiltinBuildParameters;
bool replaceAssetPathWithAddress = builtinBuildParameters.ReplaceAssetPathWithAddress;
CreateManifestFile(true, true, replaceAssetPathWithAddress, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)

View File

@@ -40,17 +40,17 @@ namespace YooAsset.Editor
throw new Exception(message);
}
}
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
}
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileCRC32Value(filePath);
}
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return FileUtility.GetFileSize(filePath);

View File

@@ -27,6 +27,12 @@ namespace YooAsset.Editor
/// </summary>
public bool IgnoreTypeTreeChanges = true;
/// <summary>
/// 使用可寻址地址代替资源路径
/// 说明:开启此项可以节省运行时清单占用的内存!
/// </summary>
public bool ReplaceAssetPathWithAddress = false;
/// <summary>
/// 获取内置构建管线的构建选项

View File

@@ -7,7 +7,7 @@ namespace YooAsset.Editor
{
void IBuildTask.Run(BuildContext context)
{
CreateManifestFile(false, false, context);
CreateManifestFile(false, true, false, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)

View File

@@ -19,16 +19,16 @@ namespace YooAsset.Editor
{
return 0;
}
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return GetFilePathTempHash(filePath);
}
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
return 0;
}
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context)
{
return GetBundleTempSize(bundleInfo);
}

View File

@@ -9,7 +9,7 @@ namespace YooAsset.Editor
{
void IBuildTask.Run(BuildContext context)
{
CreateManifestFile(false, true, context);
CreateManifestFile(false, true, false, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)

View File

@@ -15,27 +15,55 @@ namespace YooAsset.Editor
protected override string GetUnityHash(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var rawFileBuildParameters = buildParametersContext.Parameters as RawFileBuildParameters;
if (rawFileBuildParameters.IncludePathInHash)
{
string filePath = bundleInfo.PackageSourceFilePath;
return GetFileMD5IncludePath(filePath);
}
else
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
}
}
protected override uint GetUnityCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
return 0;
}
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var rawFileBuildParameters = buildParametersContext.Parameters as RawFileBuildParameters;
if (rawFileBuildParameters.IncludePathInHash)
{
string filePath = bundleInfo.PackageSourceFilePath;
return GetFileMD5IncludePath(filePath);
}
else
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
}
}
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileCRC32Value(filePath);
}
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return FileUtility.GetFileSize(filePath);
}
private string GetFileMD5IncludePath(string filePath)
{
string pathHash = HashUtility.StringMD5(filePath.ToLowerInvariant());
string contentHash = HashUtility.FileMD5(filePath);
string combined = pathHash + contentHash;
return HashUtility.StringMD5(combined);
}
}
}

View File

@@ -6,5 +6,9 @@ namespace YooAsset.Editor
{
public class RawFileBuildParameters : BuildParameters
{
/// <summary>
/// 文件哈希值计算包含路径信息
/// </summary>
public bool IncludePathInHash = false;
}
}

View File

@@ -24,7 +24,8 @@ namespace YooAsset.Editor
var scriptableBuildParameters = buildParametersContext.Parameters as ScriptableBuildParameters;
// 构建内容
var buildContent = new BundleBuildContent(buildMapContext.GetPipelineBuilds());
var bundleBuilds = buildMapContext.GetPipelineBuilds(scriptableBuildParameters.ReplaceAssetPathWithAddress);
var buildContent = new BundleBuildContent(bundleBuilds);
// 开始构建
IBundleBuildResults buildResults;

View File

@@ -13,7 +13,10 @@ namespace YooAsset.Editor
void IBuildTask.Run(BuildContext context)
{
CreateManifestFile(true, true, context);
var buildParametersContext = context.GetContextObject<BuildParametersContext>();
var scriptableBuildParameters = buildParametersContext.Parameters as ScriptableBuildParameters;
bool replaceAssetPathWithAddress = scriptableBuildParameters.ReplaceAssetPathWithAddress;
CreateManifestFile(true, true, replaceAssetPathWithAddress, context);
}
protected override string[] GetBundleDepends(BuildContext context, string bundleName)

View File

@@ -40,17 +40,17 @@ namespace YooAsset.Editor
throw new Exception(message);
}
}
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override string GetBundleFileHash(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileMD5(filePath);
}
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override uint GetBundleFileCRC(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return HashUtility.FileCRC32Value(filePath);
}
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildParametersContext buildParametersContext)
protected override long GetBundleFileSize(BuildBundleInfo bundleInfo, BuildContext context)
{
string filePath = bundleInfo.PackageSourceFilePath;
return FileUtility.GetFileSize(filePath);

View File

@@ -29,6 +29,12 @@ namespace YooAsset.Editor
/// </summary>
public bool IgnoreTypeTreeChanges = true;
/// <summary>
/// 使用可寻址地址代替资源路径
/// 说明:开启此项可以节省运行时清单占用的内存!
/// </summary>
public bool ReplaceAssetPathWithAddress = false;
/// <summary>
/// 自动建立资源对象对图集的依赖关系
/// </summary>

View File

@@ -21,6 +21,11 @@ namespace YooAsset.Editor
/// </summary>
public string ErrorInfo;
/// <summary>
/// 构建失败的堆栈
/// </summary>
public string ErrorStack;
/// <summary>
/// 输出的补丁包目录
/// </summary>

View File

@@ -51,6 +51,7 @@ namespace YooAsset.Editor
EditorTools.ClearProgressBar();
buildResult.FailedTask = task.GetType().Name;
buildResult.ErrorInfo = e.ToString();
buildResult.ErrorStack = e.StackTrace;
buildResult.Success = false;
break;
}

View File

@@ -1,9 +1,9 @@

namespace YooAsset.Editor
{
public class EncryptionNone : IEncryptionServices
public class EncryptionNone : IBundleEncryptionServices
{
public EncryptResult Encrypt(EncryptFileInfo fileInfo)
public EncryptResult Encrypt(EncryptBundleInfo fileInfo)
{
throw new System.NotImplementedException();
}

View File

@@ -46,13 +46,13 @@ namespace YooAsset.Editor
/// <summary>
/// 创建资源包加密服务类实例
/// </summary>
protected IEncryptionServices CreateEncryptionServicesInstance()
protected IBundleEncryptionServices CreateEncryptionServicesInstance()
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
var classTypes = EditorTools.GetAssignableTypes(typeof(IBundleEncryptionServices));
var classType = classTypes.Find(x => x.FullName.Equals(className));
if (classType != null)
return (IEncryptionServices)Activator.CreateInstance(classType);
return (IBundleEncryptionServices)Activator.CreateInstance(classType);
else
return null;
}
@@ -184,7 +184,7 @@ namespace YooAsset.Editor
protected PopupField<Type> CreateEncryptionServicesField(VisualElement container)
{
// 资源包加密服务类
var classTypes = EditorTools.GetAssignableTypes(typeof(IEncryptionServices));
var classTypes = EditorTools.GetAssignableTypes(typeof(IBundleEncryptionServices));
if (classTypes.Count > 0)
{
var className = AssetBundleBuilderSetting.GetPackageEncyptionServicesClassName(PackageName, PipelineName);

View File

@@ -82,14 +82,14 @@ namespace YooAsset.Editor
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("提示", $"开始构建资源包[{PackageName}]", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] 打包已经取消");
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
@@ -109,7 +109,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildBundleType = (int)EBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -42,14 +42,14 @@ namespace YooAsset.Editor
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("提示", $"开始构建资源包[{PackageName}]", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] 打包已经取消");
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
@@ -66,7 +66,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.VirtualBundle;
buildParameters.BuildBundleType = (int)EBundleType.VirtualBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -77,14 +77,14 @@ namespace YooAsset.Editor
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("提示", $"开始构建资源包[{PackageName}]", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] 打包已经取消");
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
@@ -103,7 +103,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.RawBundle;
buildParameters.BuildBundleType = (int)EBundleType.RawBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -82,14 +82,14 @@ namespace YooAsset.Editor
}
private void BuildButton_clicked()
{
if (EditorUtility.DisplayDialog("提示", $"开始构建资源包[{PackageName}]", "Yes", "No"))
if (EditorUtility.DisplayDialog("Info", $"Start building resource package [{PackageName}]!", "Yes", "No"))
{
EditorTools.ClearUnityConsole();
EditorApplication.delayCall += ExecuteBuild;
}
else
{
Debug.LogWarning("[Build] 打包已经取消");
Debug.LogWarning("[Build] Packaging has been canceled.");
}
}
@@ -109,7 +109,7 @@ namespace YooAsset.Editor
buildParameters.BuildOutputRoot = AssetBundleBuilderHelper.GetDefaultBuildOutputRoot();
buildParameters.BuildinFileRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot();
buildParameters.BuildPipeline = PipelineName.ToString();
buildParameters.BuildBundleType = (int)EBuildBundleType.AssetBundle;
buildParameters.BuildBundleType = (int)EBundleType.AssetBundle;
buildParameters.BuildTarget = BuildTarget;
buildParameters.PackageName = PackageName;
buildParameters.PackageVersion = _buildVersionField.value;

View File

@@ -139,14 +139,30 @@ namespace YooAsset.Editor
/// </summary>
public List<CollectAssetInfo> GetAllCollectAssets(CollectCommand command, AssetBundleCollectorGroup group)
{
bool ignoreStaticCollector = command.IsFlagSet(ECollectFlags.IgnoreStaticCollector);
if (ignoreStaticCollector)
{
if (CollectorType == ECollectorType.StaticAssetCollector)
return new List<CollectAssetInfo>();
}
bool ignoreDependCollector = command.IsFlagSet(ECollectFlags.IgnoreDependCollector);
if (ignoreDependCollector)
{
if (CollectorType == ECollectorType.DependAssetCollector)
return new List<CollectAssetInfo>();
}
Dictionary<string, CollectAssetInfo> result = new Dictionary<string, CollectAssetInfo>(1000);
// 收集打包资源路径
List<string> findAssets = new List<string>();
if (AssetDatabase.IsValidFolder(CollectPath))
{
string collectDirectory = CollectPath;
string[] findResult = EditorTools.FindAssets(EAssetSearchType.All, collectDirectory);
IFilterRule filterRuleInstance = AssetBundleCollectorSettingData.GetFilterRuleInstance(FilterRuleName);
string findAssetType = filterRuleInstance.FindAssetType;
string searchFolder = CollectPath;
string[] findResult = EditorTools.FindAssets(findAssetType, searchFolder);
findAssets.AddRange(findResult);
}
else
@@ -262,8 +278,8 @@ namespace YooAsset.Editor
}
private List<AssetInfo> GetAllDependencies(CollectCommand command, string mainAssetPath)
{
// 注意:模拟构建模式下不需要收集依赖资源
if (command.SimulateBuild)
bool ignoreGetDependencies = command.IsFlagSet(ECollectFlags.IgnoreGetDependencies);
if (ignoreGetDependencies)
return new List<AssetInfo>();
string[] depends = command.AssetDependency.GetDependencies(mainAssetPath, true);

View File

@@ -236,6 +236,7 @@ namespace YooAsset.Editor
// 包裹名称
_packageNameTxt = root.Q<TextField>("PackageName");
_packageNameTxt.isDelayed = true;
_packageNameTxt.RegisterValueChangedCallback(evt =>
{
var selectPackage = _packageListView.selectedItem as AssetBundleCollectorPackage;
@@ -249,6 +250,7 @@ namespace YooAsset.Editor
// 包裹备注
_packageDescTxt = root.Q<TextField>("PackageDesc");
_packageDescTxt.isDelayed = true;
_packageDescTxt.RegisterValueChangedCallback(evt =>
{
var selectPackage = _packageListView.selectedItem as AssetBundleCollectorPackage;
@@ -286,6 +288,7 @@ namespace YooAsset.Editor
// 分组名称
_groupNameTxt = root.Q<TextField>("GroupName");
_groupNameTxt.isDelayed = true;
_groupNameTxt.RegisterValueChangedCallback(evt =>
{
var selectPackage = _packageListView.selectedItem as AssetBundleCollectorPackage;
@@ -300,6 +303,7 @@ namespace YooAsset.Editor
// 分组备注
_groupDescTxt = root.Q<TextField>("GroupDesc");
_groupDescTxt.isDelayed = true;
_groupDescTxt.RegisterValueChangedCallback(evt =>
{
var selectPackage = _packageListView.selectedItem as AssetBundleCollectorPackage;
@@ -314,6 +318,7 @@ namespace YooAsset.Editor
// 分组的资源标签
_groupTagsTxt = root.Q<TextField>("GroupTags");
_groupTagsTxt.isDelayed = true;
_groupTagsTxt.RegisterValueChangedCallback(evt =>
{
var selectPackage = _packageListView.selectedItem as AssetBundleCollectorPackage;
@@ -817,6 +822,7 @@ namespace YooAsset.Editor
var textField = new TextField();
textField.name = "TextField0";
textField.label = "User Data";
textField.isDelayed = true;
textField.style.width = 200;
elementBottom.Add(textField);
var label = textField.Q<Label>();
@@ -826,6 +832,7 @@ namespace YooAsset.Editor
var textField = new TextField();
textField.name = "TextField1";
textField.label = "Asset Tags";
textField.isDelayed = true;
textField.style.width = 100;
textField.style.marginLeft = 20;
textField.style.flexGrow = 1;
@@ -1028,7 +1035,7 @@ namespace YooAsset.Editor
IIgnoreRule ignoreRule = AssetBundleCollectorSettingData.GetIgnoreRuleInstance(_ignoreRulePopupField.value.ClassName);
string packageName = _packageNameTxt.value;
var command = new CollectCommand(packageName, ignoreRule);
command.SimulateBuild = true;
command.SetFlag(ECollectFlags.IgnoreGetDependencies, true);
command.UniqueBundleName = _uniqueBundleNameToogle.value;
command.EnableAddressable = _enableAddressableToogle.value;
command.SupportExtensionless = _supportExtensionlessToogle.value;

View File

@@ -30,7 +30,7 @@
<ui:VisualElement name="PackageContainer" style="width: 200px; flex-grow: 0; background-color: rgb(67, 67, 67); border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px;">
<ui:Label text="Packages" display-tooltip-when-elided="true" name="PackageTitle" style="background-color: rgb(89, 89, 89); -unity-text-align: upper-center; -unity-font-style: bold; border-left-width: 5px; border-right-width: 5px; border-top-width: 5px; border-bottom-width: 5px; font-size: 12px;" />
<ui:ListView focusable="true" name="PackageListView" item-height="20" virtualization-method="DynamicHeight" reorderable="true" reorder-mode="Animated" style="flex-grow: 1;" />
<ui:VisualElement name="PackageAddContainer" style="height: 20px; flex-direction: row; justify-content: center;">
<ui:VisualElement name="PackageAddContainer" style="height: 20px; flex-direction: row; justify-content: center; flex-shrink: 0;">
<ui:Button text=" - " display-tooltip-when-elided="true" name="RemoveBtn" />
<ui:Button text=" + " display-tooltip-when-elided="true" name="AddBtn" />
</ui:VisualElement>
@@ -40,7 +40,7 @@
<ui:TextField picking-mode="Ignore" label="Package Name" name="PackageName" style="flex-direction: column;" />
<ui:TextField picking-mode="Ignore" label="Package Desc" name="PackageDesc" style="flex-direction: column;" />
<ui:ListView focusable="true" name="GroupListView" item-height="20" virtualization-method="DynamicHeight" reorderable="true" reorder-mode="Animated" style="flex-grow: 1;" />
<ui:VisualElement name="GroupAddContainer" style="height: 20px; flex-direction: row; justify-content: center;">
<ui:VisualElement name="GroupAddContainer" style="height: 20px; flex-direction: row; justify-content: center; flex-shrink: 0;">
<ui:Button text=" - " display-tooltip-when-elided="true" name="RemoveBtn" />
<ui:Button text=" + " display-tooltip-when-elided="true" name="AddBtn" />
</ui:VisualElement>

View File

@@ -88,8 +88,8 @@ namespace YooAsset.Editor
}
catch (Exception ex)
{
ClearDatabase(true);
Debug.LogError($"Failed to load cache database : {ex.Message}");
ClearDatabase(true);
}
finally
{
@@ -169,7 +169,8 @@ namespace YooAsset.Editor
File.Delete(_databaseFilePath);
}
_database.Clear();
if (_database != null)
_database.Clear();
}
/// <summary>

View File

@@ -1,6 +1,26 @@

namespace YooAsset.Editor
{
public enum ECollectFlags
{
None = 0,
/// <summary>
/// 不收集依赖资源
/// </summary>
IgnoreGetDependencies = 1 << 0,
/// <summary>
/// 忽略静态收集器
/// </summary>
IgnoreStaticCollector = 1 << 1,
/// <summary>
/// 忽略依赖收集器
/// </summary>
IgnoreDependCollector = 1 << 2,
}
public class CollectCommand
{
/// <summary>
@@ -17,7 +37,20 @@ namespace YooAsset.Editor
/// <summary>
/// 模拟构建模式
/// </summary>
public bool SimulateBuild { set; get; }
public bool SimulateBuild
{
set
{
SetFlag(ECollectFlags.IgnoreGetDependencies, value);
SetFlag(ECollectFlags.IgnoreStaticCollector, value);
SetFlag(ECollectFlags.IgnoreDependCollector, value);
}
}
/// <summary>
/// 窗口收集模式
/// </summary>
public int CollectFlags { set; get; } = 0;
/// <summary>
/// 资源包名唯一化
@@ -70,5 +103,24 @@ namespace YooAsset.Editor
PackageName = packageName;
IgnoreRule = ignoreRule;
}
/// <summary>
/// 设置标记位
/// </summary>
public void SetFlag(ECollectFlags flag, bool isOn)
{
if (isOn)
CollectFlags |= (int)flag; // 开启指定标志位
else
CollectFlags &= ~(int)flag; // 关闭指定标志位
}
/// <summary>
/// 查询标记位
/// </summary>
public bool IsFlagSet(ECollectFlags flag)
{
return (CollectFlags & (int)flag) != 0;
}
}
}

View File

@@ -23,7 +23,13 @@ namespace YooAsset.Editor
public interface IFilterRule
{
/// <summary>
/// 是否为收集资源
/// 搜寻的资源类型
/// 说明:使用引擎方法搜索获取所有资源列表
/// </summary>
string FindAssetType { get; }
/// <summary>
/// 验证搜寻的资源是否为收集资源
/// </summary>
/// <returns>如果收集该资源返回TRUE</returns>
bool IsCollectAsset(FilterRuleData data);

View File

@@ -9,6 +9,11 @@ namespace YooAsset.Editor
[DisplayName("收集所有资源")]
public class CollectAll : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.All.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return true;
@@ -18,6 +23,11 @@ namespace YooAsset.Editor
[DisplayName("收集场景")]
public class CollectScene : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Scene.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
string extension = Path.GetExtension(data.AssetPath);
@@ -28,6 +38,11 @@ namespace YooAsset.Editor
[DisplayName("收集预制体")]
public class CollectPrefab : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Prefab.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".prefab";
@@ -37,6 +52,11 @@ namespace YooAsset.Editor
[DisplayName("收集精灵类型的纹理")]
public class CollectSprite : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Sprite.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
var mainAssetType = AssetDatabase.GetMainAssetTypeAtPath(data.AssetPath);
@@ -58,6 +78,11 @@ namespace YooAsset.Editor
[DisplayName("收集着色器")]
public class CollectShader : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.Shader.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".shader";
@@ -67,6 +92,11 @@ namespace YooAsset.Editor
[DisplayName("收集着色器变种集合")]
public class CollectShaderVariants : IFilterRule
{
public string FindAssetType
{
get { return EAssetSearchType.All.ToString(); }
}
public bool IsCollectAsset(FilterRuleData data)
{
return Path.GetExtension(data.AssetPath) == ".shadervariants";

View File

@@ -81,6 +81,7 @@ namespace YooAsset.Editor
public ECompressOption CompressOption;
public bool DisableWriteTypeTree;
public bool IgnoreTypeTreeChanges;
public bool ReplaceAssetPathWithAddress;
public bool WriteLinkXML = true;
public string CacheServerHost;
public int CacheServerPort;

View File

@@ -277,7 +277,7 @@ namespace YooAsset.Editor
if (dependTableData.BundleInfo.Encrypted)
return;
if (_buildReport.Summary.BuildBundleType == (int)EBuildBundleType.AssetBundle)
if (_buildReport.Summary.BuildBundleType == (int)EBundleType.AssetBundle)
{
string rootDirectory = Path.GetDirectoryName(_reportFilePath);
string filePath = $"{rootDirectory}/{dependTableData.BundleInfo.FileName}";

View File

@@ -348,7 +348,7 @@ namespace YooAsset.Editor
if (bundleTableData.BundleInfo.Encrypted)
return;
if (_buildReport.Summary.BuildBundleType == (int)EBuildBundleType.AssetBundle)
if (_buildReport.Summary.BuildBundleType == (int)EBundleType.AssetBundle)
{
string rootDirectory = Path.GetDirectoryName(_reportFilePath);
string filePath = $"{rootDirectory}/{bundleTableData.BundleInfo.FileName}";

View File

@@ -74,6 +74,7 @@ namespace YooAsset.Editor
BindListViewItem("CompressOption", $"{buildReport.Summary.CompressOption}");
BindListViewItem("DisableWriteTypeTree", $"{buildReport.Summary.DisableWriteTypeTree}");
BindListViewItem("IgnoreTypeTreeChanges", $"{buildReport.Summary.IgnoreTypeTreeChanges}");
BindListViewItem("ReplaceAssetPathWithAddress", $"{buildReport.Summary.ReplaceAssetPathWithAddress}");
BindListViewItem(string.Empty, string.Empty);
BindListViewHeader("Build Results");

View File

@@ -39,7 +39,10 @@ namespace YooAsset.Editor
Shader,
Sprite,
Texture,
RenderTexture,
VideoClip,
PlayableAsset,
TimelineAsset
}
/// <summary>

View File

@@ -169,6 +169,28 @@ namespace YooAsset.Editor
/// <param name="searchInFolders">指定搜索的文件夹列表</param>
/// <returns>返回搜集到的资源路径列表</returns>
public static string[] FindAssets(EAssetSearchType searchType, string[] searchInFolders)
{
return FindAssets(searchType.ToString(), searchInFolders);
}
/// <summary>
/// 搜集资源
/// </summary>
/// <param name="searchType">搜集的资源类型</param>
/// <param name="searchInFolder">指定搜索的文件夹</param>
/// <returns>返回搜集到的资源路径列表</returns>
public static string[] FindAssets(EAssetSearchType searchType, string searchInFolder)
{
return FindAssets(searchType.ToString(), new string[] { searchInFolder });
}
/// <summary>
/// 搜集资源
/// </summary>
/// <param name="searchType">搜集的资源类型</param>
/// <param name="searchInFolders">指定搜索的文件夹列表</param>
/// <returns>返回搜集到的资源路径列表</returns>
public static string[] FindAssets(string searchType, string[] searchInFolders)
{
// 注意AssetDatabase.FindAssets()不支持末尾带分隔符的文件夹路径
for (int i = 0; i < searchInFolders.Length; i++)
@@ -179,7 +201,7 @@ namespace YooAsset.Editor
// 注意:获取指定目录下的所有资源对象(包括子文件夹)
string[] guids;
if (searchType == EAssetSearchType.All)
if (string.IsNullOrEmpty(searchType) || searchType == EAssetSearchType.All.ToString())
guids = AssetDatabase.FindAssets(string.Empty, searchInFolders);
else
guids = AssetDatabase.FindAssets($"t:{searchType}", searchInFolders);
@@ -206,7 +228,7 @@ namespace YooAsset.Editor
/// <param name="searchType">搜集的资源类型</param>
/// <param name="searchInFolder">指定搜索的文件夹</param>
/// <returns>返回搜集到的资源路径列表</returns>
public static string[] FindAssets(EAssetSearchType searchType, string searchInFolder)
public static string[] FindAssets(string searchType, string searchInFolder)
{
return FindAssets(searchType, new string[] { searchInFolder });
}

View File

@@ -0,0 +1,945 @@
# DiagnosticSystem 诊断系统
## 模块概述
DiagnosticSystem 是 YooAsset 的**远程调试诊断系统**提供运行时资源管理状态的实时可视化和性能分析能力。该系统通过编辑器窗口与运行时游戏进行双向通信实时采集和展示资源加载、Bundle 管理、异步操作等调试信息。
### 核心特性
- **实时远程调试**:在 Unity 编辑器中查看游戏运行时的资源管理状态
- **完整状态快照**采集所有资源、Bundle、异步操作的实时信息
- **历史数据回溯**:缓存最近 500 帧数据,支持时间回溯分析
- **双模式采样**:支持单次采样和自动连续采样
- **低性能开销**:按需采样,无需连续监控
### 模块统计
| 组件 | 职责 |
|------|------|
| 核心通信 | RemoteDebuggerInRuntime + 双连接层 |
| 数据结构 | 5 个调试信息结构体 |
| 命令系统 | RemoteCommand 命令定义 |
| **总计** | 10 个核心文件,完整的远程诊断框架 |
---
## 设计目标
| 目标 | 说明 |
|------|------|
| **实时可视化** | 在编辑器中实时查看运行时资源状态 |
| **性能监控** | 收集加载耗时、引用计数、下载进度等性能指标 |
| **内存诊断** | 追踪资源出生场景、引用计数,识别潜在内存泄漏 |
| **操作追踪** | 显示异步操作树,包括嵌套和依赖关系 |
| **低开销设计** | 按需采样而非连续监控DEBUG 模式自动启用 |
---
## 文件结构
```
DiagnosticSystem/
├── RemoteDebuggerDefine.cs # 全局定义和常量
├── RemoteCommand.cs # 命令定义和序列化
├── DebugReport.cs # 调试报告(顶层容器)
├── DebugPackageData.cs # 包级调试数据
├── DebugProviderInfo.cs # 资源加载器调试信息
├── DebugBundleInfo.cs # 资源包调试信息
├── DebugOperationInfo.cs # 异步操作调试信息
├── RemoteDebuggerInRuntime.cs # 运行时调试器主类
├── RemotePlayerConnection.cs # 编辑器模拟连接层
└── RemoteEditorConnection.cs # 运行时模拟连接层
```
---
## 核心类说明
### RemoteDebuggerDefine
全局定义类,包含调试器版本和通信协议的 GUID 标识符。
```csharp
internal class RemoteDebuggerDefine
{
// 调试器版本(用于版本校验)
public const string DebuggerVersion = "2.3.3";
// 消息标识符GUID
public static readonly Guid kMsgPlayerSendToEditor =
new Guid("e34a5702dd353724aa315fb8011f08c3"); // 运行时→编辑器
public static readonly Guid kMsgEditorSendToPlayer =
new Guid("4d1926c9df5b052469a1c63448b7609a"); // 编辑器→运行时
}
```
### RemoteCommand
命令定义类,用于编辑器向运行时发送采样指令。
```csharp
internal enum ERemoteCommand
{
SampleOnce = 0, // 单次采样
SampleAuto = 1, // 自动采样(连续)
}
[Serializable]
internal class RemoteCommand
{
public int CommandType; // ERemoteCommand 枚举值
public string CommandParam; // 命令参数
// 序列化/反序列化JSON 格式)
public static byte[] Serialize(RemoteCommand command)
{
return Encoding.UTF8.GetBytes(JsonUtility.ToJson(command));
}
public static RemoteCommand Deserialize(byte[] data)
{
return JsonUtility.FromJson<RemoteCommand>(Encoding.UTF8.GetString(data));
}
}
```
**命令示例:**
```json
// 单次采样
{
"CommandType": 0,
"CommandParam": ""
}
// 开启自动采样
{
"CommandType": 1,
"CommandParam": "open"
}
// 关闭自动采样
{
"CommandType": 1,
"CommandParam": "close"
}
```
### DebugReport
调试报告容器,包含完整的系统状态快照。
```csharp
[Serializable]
internal class DebugReport
{
// 调试器版本(用于版本校验)
public string DebuggerVersion = RemoteDebuggerDefine.DebuggerVersion;
// 游戏帧数
public int FrameCount;
// 包级调试数据列表(一个游戏可能有多个资源包)
public List<DebugPackageData> PackageDatas = new List<DebugPackageData>(10);
// 序列化/反序列化
public static byte[] Serialize(DebugReport debugReport);
public static DebugReport Deserialize(byte[] data);
}
```
### DebugPackageData
包级调试数据,包含单个资源包的所有诊断信息。
```csharp
[Serializable]
internal class DebugPackageData
{
// 资源包名称
public string PackageName;
// 资源加载器列表
public List<DebugProviderInfo> ProviderInfos = new List<DebugProviderInfo>(1000);
// 资源包列表
public List<DebugBundleInfo> BundleInfos = new List<DebugBundleInfo>(1000);
// 异步操作列表
public List<DebugOperationInfo> OperationInfos = new List<DebugOperationInfo>(1000);
// 运行时查询字典(非序列化,按需构建)
[NonSerialized]
public Dictionary<string, DebugBundleInfo> BundleInfoDic;
// 延迟解析字典
public DebugBundleInfo GetBundleInfo(string bundleName);
}
```
### DebugProviderInfo
资源加载器Provider的调试信息。
```csharp
[Serializable]
internal struct DebugProviderInfo : IComparer<DebugProviderInfo>, IComparable<DebugProviderInfo>
{
public string PackageName; // 所属包名
public string AssetPath; // 资源路径(如 "Assets/Prefabs/Player.prefab"
public string SpawnScene; // 资源加载时的活跃场景名
public string BeginTime; // 加载开始时间格式HH:mm:ss.fff
public long LoadingTime; // 加载耗时(单位:毫秒)
public int RefCount; // 引用计数
public string Status; // 加载状态None/Processing/Succeed/Failed
public List<string> DependBundles; // 依赖的资源包名列表
// 按 AssetPath 字母排序
public int CompareTo(DebugProviderInfo other);
}
```
**关键诊断价值:**
- `SpawnScene`:追踪资源在哪个场景被加载,帮助识别资源泄漏
- `LoadingTime`:性能分析,识别加载慢的资源
- `RefCount`引用计数监控RefCount > 0 表示资源仍在使用
- `DependBundles`:依赖分析,理解资源加载的完整依赖链
### DebugBundleInfo
资源包Bundle的调试信息。
```csharp
[Serializable]
internal struct DebugBundleInfo : IComparer<DebugBundleInfo>, IComparable<DebugBundleInfo>
{
public string BundleName; // 资源包名称
public int RefCount; // 引用计数(当前有多少个 Provider 在使用)
public string Status; // 加载状态
public List<string> ReferenceBundles; // 反向依赖(谁引用了我)
// 按 BundleName 字母排序
public int CompareTo(DebugBundleInfo other);
}
```
**关键诊断价值:**
- `RefCount`Bundle 引用计数,为 0 时可以被卸载
- `ReferenceBundles`:反向依赖分析,了解 Bundle 被哪些其他 Bundle 依赖
### DebugOperationInfo
异步操作的调试信息,支持递归树结构。
```csharp
[Serializable]
internal struct DebugOperationInfo : IComparer<DebugOperationInfo>, IComparable<DebugOperationInfo>
{
public string OperationName; // 操作类名(如 "LoadAssetOperation"
public string OperationDesc; // 操作说明(自定义描述)
public uint Priority; // 优先级(用于操作排序)
public float Progress; // 进度0.0 - 1.0
public string BeginTime; // 操作开始时间
public long ProcessTime; // 处理耗时(单位:毫秒)
public string Status; // 操作状态None/Processing/Succeed/Failed
public List<DebugOperationInfo> Childs; // 子操作列表(支持嵌套树结构)
public int CompareTo(DebugOperationInfo other);
}
```
**递归树结构示例:**
```
InitializationOperation
├─ LoadManifestOperation
│ ├─ LoadBundleFileOperation (manifest.bundle)
│ └─ DeserializeManifestOperation
└─ InitFileSystemOperation
```
**关键诊断价值:**
- `OperationName`:操作类型识别
- `ProcessTime`:性能瓶颈分析
- `Childs`:操作依赖关系可视化
---
## 通信系统
### RemoteDebuggerInRuntime
运行时调试器主类,负责接收命令、采样数据、发送报告。
```csharp
internal class RemoteDebuggerInRuntime : MonoBehaviour
{
// 采样控制标志
private static bool _sampleOnce = false; // 单次采样
private static bool _autoSample = false; // 连续采样
// 运行时初始化
[RuntimeInitializeOnLoadMethod]
private static void RuntimeInitializeOnLoad()
{
_sampleOnce = false;
_autoSample = false;
}
private void Awake()
{
RemotePlayerConnection.Instance.Initialize();
}
private void OnEnable()
{
// 注册命令接收回调
RemotePlayerConnection.Instance.Register(
RemoteDebuggerDefine.kMsgEditorSendToPlayer,
OnHandleEditorMessage);
}
private void LateUpdate()
{
// 采样逻辑(在一帧的最后执行)
if (_autoSample || _sampleOnce)
{
_sampleOnce = false;
var debugReport = YooAssets.GetDebugReport();
var data = DebugReport.Serialize(debugReport);
RemotePlayerConnection.Instance.Send(
RemoteDebuggerDefine.kMsgPlayerSendToEditor,
data);
}
}
private static void OnHandleEditorMessage(MessageEventArgs args)
{
var command = RemoteCommand.Deserialize(args.data);
if (command.CommandType == (int)ERemoteCommand.SampleOnce)
{
_sampleOnce = true;
}
else if (command.CommandType == (int)ERemoteCommand.SampleAuto)
{
_autoSample = (command.CommandParam == "open");
}
}
}
```
**关键设计点:**
1. **LateUpdate 时机**:确保该帧所有资源加载完成后再采样
2. **状态重置**`[RuntimeInitializeOnLoadMethod]` 确保编辑器重新编译时重置状态
3. **DEBUG 模式自动启用**:通过 `#if DEBUG` 条件编译自动添加组件
### 双连接层架构
YooAsset 支持两种通信模式:
| 模式 | 使用场景 | 实现方式 |
|------|----------|----------|
| **编辑器模拟模式** | 开发调试 | `RemotePlayerConnection` + `RemoteEditorConnection`(虚拟连接) |
| **发布版本** | 运营期监控 | Unity 的 `PlayerConnection` API真实网络 |
#### RemotePlayerConnection编辑器模拟模式
```csharp
internal class RemotePlayerConnection
{
private static RemotePlayerConnection _instance;
private readonly Dictionary<Guid, UnityAction<MessageEventArgs>> _messageCallbacks;
public static RemotePlayerConnection Instance
{
get
{
if (_instance == null)
_instance = new RemotePlayerConnection();
return _instance;
}
}
public void Register(Guid messageID, UnityAction<MessageEventArgs> callback)
{
_messageCallbacks.Add(messageID, callback);
}
public void Send(Guid messageID, byte[] data)
{
// 在编辑器模拟模式下,发送给虚拟编辑器连接
RemoteEditorConnection.Instance.HandlePlayerMessage(messageID, data);
}
internal void HandleEditorMessage(Guid messageID, byte[] data)
{
if (_messageCallbacks.TryGetValue(messageID, out var callback))
{
callback.Invoke(new MessageEventArgs { playerId = 0, data = data });
}
}
}
```
#### RemoteEditorConnection运行时模拟模式
```csharp
internal class RemoteEditorConnection
{
private static RemoteEditorConnection _instance;
private readonly Dictionary<Guid, UnityAction<MessageEventArgs>> _messageCallbacks;
public static RemoteEditorConnection Instance
{
get
{
if (_instance == null)
_instance = new RemoteEditorConnection();
return _instance;
}
}
public void Register(Guid messageID, UnityAction<MessageEventArgs> callback)
{
_messageCallbacks.Add(messageID, callback);
}
public void Send(Guid messageID, byte[] data)
{
// 发送给虚拟运行时连接
RemotePlayerConnection.Instance.HandleEditorMessage(messageID, data);
}
internal void HandlePlayerMessage(Guid messageID, byte[] data)
{
if (_messageCallbacks.TryGetValue(messageID, out var callback))
{
callback.Invoke(new MessageEventArgs { playerId = 0, data = data });
}
}
}
```
---
## 通信协议
### 协议规范
**协议版本:** 2.3.3
**编码格式:**
```
C# 对象 → JsonUtility.ToJson() → JSON 字符串 → Encoding.UTF8.GetBytes() → byte[]
```
**消息类型:**
| 方向 | GUID | 数据类型 | 说明 |
|------|------|----------|------|
| 编辑器→运行时 | `4d1926c9df5b052469a1c63448b7609a` | `RemoteCommand` | 采样命令 |
| 运行时→编辑器 | `e34a5702dd353724aa315fb8011f08c3` | `DebugReport` | 调试报告 |
### 双向通信流程
```
[编辑器 UI]
├─ 用户点击 "Sample" 按钮
│ └─ 发送 RemoteCommand (SampleOnce)
└─ 用户开启 "Record" 开关
└─ 发送 RemoteCommand (SampleAuto, "open")
↓ RemoteEditorConnection.Send()
RemotePlayerConnection.HandleEditorMessage()
↓ 触发回调
[运行时]
RemoteDebuggerInRuntime.OnHandleEditorMessage()
设置采样标志 (_sampleOnce 或 _autoSample)
LateUpdate 中采样
YooAssets.GetDebugReport()
├─ 收集所有 ResourcePackage 数据
├─ DebugProviderInfo[] (从 ProviderDic)
├─ DebugBundleInfo[] (从 LoaderDic)
└─ DebugOperationInfo[] (从 _operations)
DebugReport.Serialize()
RemotePlayerConnection.Send()
RemoteEditorConnection.HandlePlayerMessage()
[编辑器]
AssetBundleDebuggerWindow.OnHandlePlayerMessage()
版本校验 (DebuggerVersion)
RemotePlayerSession.AddDebugReport()
缓存到历史记录 (最多 500 帧)
UI 刷新显示
```
### 版本校验机制
```csharp
// 编辑器端版本校验
private void OnHandlePlayerMessage(MessageEventArgs args)
{
var debugReport = DebugReport.Deserialize(args.data);
// 版本校验
if (debugReport.DebuggerVersion != RemoteDebuggerDefine.DebuggerVersion)
{
Debug.LogWarning(
$"Debugger versions are inconsistent : " +
$"{debugReport.DebuggerVersion} != {RemoteDebuggerDefine.DebuggerVersion}");
return; // 丢弃不兼容的数据
}
// 处理数据...
}
```
**设计意图:** 防止编辑器和运行时的调试器版本不一致导致的数据格式错误。
---
## 数据收集流程
### 完整采样流程
```
RemoteDebuggerInRuntime.LateUpdate()
检查 _sampleOnce 或 _autoSample 标志
↓ YES
调用 YooAssets.GetDebugReport()
├─ 初始化 DebugReport
├─ 设置 FrameCount = Time.frameCount
├─ 遍历每个 ResourcePackage
│ │
│ └─ package.GetDebugPackageData()
│ │
│ ├─ 创建 DebugPackageData
│ ├─ 设置 PackageName
│ │
│ ├─ 收集 ProviderInfos
│ │ ResourceManager.GetDebugProviderInfos()
│ │ 遍历 ProviderDic
│ │ └─ 每个 ProviderOperation 提供:
│ │ - MainAssetInfo.AssetPath
│ │ - SpawnScene场景名
│ │ - BeginTime开始时间
│ │ - ProcessTime耗时
│ │ - RefCount引用计数
│ │ - Status加载状态
│ │ - GetDebugDependBundles()(依赖列表)
│ │
│ ├─ 收集 BundleInfos
│ │ ResourceManager.GetDebugBundleInfos()
│ │ 遍历 LoaderDic
│ │ └─ 每个 LoadBundleFileOperation 提供:
│ │ - BundleName
│ │ - RefCount
│ │ - Status
│ │ - FilterReferenceBundles()(反向依赖)
│ │
│ └─ 收集 OperationInfos
│ OperationSystem.GetDebugOperationInfos(PackageName)
│ 遍历 _operations按 PackageName 过滤):
│ └─ 递归构建操作树:
│ - GetType().Name操作类名
│ - GetOperationDesc()(自定义描述)
│ - Priority优先级
│ - Progress进度
│ - BeginTime开始时间
│ - ProcessTime耗时
│ - Status状态
│ - Childs子操作列表
└─ 返回 DebugReport
```
### 性能指标收集
#### 资源加载耗时
```csharp
// AsyncOperationBase 中的自动计时
private Stopwatch _watch = null;
internal void InternalStart()
{
if (_watch == null)
{
BeginTime = SpawnTimeToString(UnityEngine.Time.realtimeSinceStartup);
_watch = Stopwatch.StartNew();
}
}
internal void InternalUpdate()
{
ProcessTime = _watch.ElapsedMilliseconds;
// ... 持续计时
}
```
#### 场景信息追踪
```csharp
// ProviderOperation.cs
[Conditional("DEBUG")] // 仅在 DEBUG 模式下启用
public void InitProviderDebugInfo()
{
SpawnScene = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
}
```
#### 引用计数监控
```csharp
// ProviderOperation 和 LoadBundleFileOperation 都维护 RefCount
public int RefCount { get; } // 当前被引用的次数
```
---
## 与其他模块的交互
```
YooAssets (全局入口)
├─ 初始化阶段:
│ #if DEBUG
│ _driver.AddComponent<RemoteDebuggerInRuntime>();
│ #endif
└─ 数据收集入口:
GetDebugReport()
├─ 遍历所有 ResourcePackage
└─ 构建 DebugReport
ResourcePackage (资源包)
└─ GetDebugPackageData()
├─ 调用 ResourceManager.GetDebugProviderInfos()
├─ 调用 ResourceManager.GetDebugBundleInfos()
└─ 调用 OperationSystem.GetDebugOperationInfos()
ResourceManager (资源管理器)
├─ GetDebugProviderInfos()
│ └─ 遍历 ProviderDic (Dictionary<string, ProviderOperation>)
└─ GetDebugBundleInfos()
└─ 遍历 LoaderDic (Dictionary<string, LoadBundleFileOperation>)
OperationSystem (操作系统)
└─ GetDebugOperationInfos(packageName)
└─ 遍历 _operations (List<AsyncOperationBase>)
└─ 递归收集子操作 (Childs)
AsyncOperationBase (异步操作基类)
├─ BeginTime操作开始时间
├─ ProcessTime累计处理耗时
├─ Status操作状态
├─ Progress进度
└─ GetOperationDesc():自定义描述
ProviderOperation (资源提供者)
├─ SpawnScene加载时的活跃场景
└─ GetDebugDependBundles():依赖包列表
LoadBundleFileOperation (Bundle 加载器)
├─ RefCount引用计数
└─ LoadBundleInfoBundle 信息
```
---
## 使用场景
### 场景 1运行时资源泄漏诊断
**问题:** 游戏切换场景后内存持续增长,怀疑有资源未释放。
**诊断步骤:**
1. 打开 AssetBundle Debugger 窗口
2. 开启 Record 模式(自动采样)
3. 切换场景前后观察 ProviderInfos 列表
4. 检查 `RefCount > 0``SpawnScene` 为旧场景的资源
5. 定位未释放的资源和对应的代码位置
**关键字段:**
- `SpawnScene`:资源在哪个场景被加载
- `RefCount`:引用计数,应该为 0
- `AssetPath`:资源路径,定位具体资源
### 场景 2资源加载性能分析
**问题:** 首次加载场景卡顿严重。
**诊断步骤:**
1. 单次采样Sample Once
2. 切换到 Asset View
3.`LoadingTime` 降序排序
4. 识别加载耗时最长的资源
5. 分析 `DependBundles` 了解依赖链
**关键字段:**
- `LoadingTime`:加载耗时(毫秒)
- `DependBundles`:依赖的 Bundle 列表
- `Status`:加载状态
### 场景 3Bundle 引用分析
**问题:** 某个 Bundle 无法被卸载。
**诊断步骤:**
1. 切换到 Bundle View
2. 搜索目标 Bundle
3. 检查 `RefCount``ReferenceBundles`
4. 追踪哪些资源正在使用该 Bundle
5. 定位未释放的资源引用
**关键字段:**
- `RefCount`Bundle 引用计数
- `ReferenceBundles`:反向依赖列表
- `Status`Bundle 加载状态
### 场景 4异步操作监控
**问题:** 复杂的初始化流程卡住,不知道在哪个步骤。
**诊断步骤:**
1. 切换到 Operation View
2. 查看操作树结构
3. 检查 `Status``Processing` 的操作
4. 分析 `Progress` 了解进度
5. 通过 `Childs` 了解操作依赖关系
**关键字段:**
- `OperationName`:操作类型
- `OperationDesc`:操作描述
- `Progress`进度0.0 - 1.0
- `Childs`:子操作列表
---
## 数据导出
### JSON 导出功能
编辑器窗口支持导出当前帧的完整调试数据为 JSON 文件。
**导出示例:**
```json
{
"DebuggerVersion": "2.3.3",
"FrameCount": 1234,
"PackageDatas": [
{
"PackageName": "DefaultPackage",
"ProviderInfos": [
{
"PackageName": "DefaultPackage",
"AssetPath": "Assets/Prefabs/Player.prefab",
"SpawnScene": "GameScene",
"BeginTime": "12:34:56.789",
"LoadingTime": 45,
"RefCount": 1,
"Status": "Succeed",
"DependBundles": [
"assets_prefabs.bundle"
]
}
],
"BundleInfos": [
{
"BundleName": "assets_prefabs.bundle",
"RefCount": 1,
"Status": "Succeed",
"ReferenceBundles": []
}
],
"OperationInfos": [
{
"OperationName": "LoadAssetOperation",
"OperationDesc": "Load assets_prefabs.bundle",
"Priority": 0,
"Progress": 1.0,
"BeginTime": "12:34:56.745",
"ProcessTime": 44,
"Status": "Succeed",
"Childs": []
}
]
}
]
}
```
**用途:**
- 离线分析和归档
- 性能数据对比
- 问题复现和追踪
---
## 系统架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ Unity Editor Window │
│ AssetBundleDebuggerWindow │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ UI Controls: │ │
│ │ - Sample Button (SampleOnce) │ │
│ │ - Record Toggle (SampleAuto) │ │
│ │ - View Mode Menu (Asset/Bundle/Operation View) │ │
│ │ - Frame Slider (历史帧导航) │ │
│ │ - Search Field (关键词搜索) │ │
│ │ - Export Button (JSON 导出) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└────────────┬──────────────────────────────────────────────────────────┘
┌─────▼───────────────────────────────────┐
│ RemoteEditorConnection (虚拟连接) │
│ - Register callbacks │
│ - Send/Receive commands & reports │
└─────┬──────────────────────────────┬────┘
│ │
┌────────▼─────────────┐ ┌──────────▼───────────────┐
│ RemoteCommand │ │ DebugReport │
│ (Serialize) │ │ (Deserialize) │
│ ↓ JSON │ │ ← JSON │
│ ↓ UTF-8 bytes │ │ ← UTF-8 bytes │
└────────┬─────────────┘ └──────────┬───────────────┘
│ │
│ ═══════════════════════ │
│ Internet / Emulation │
│ ═══════════════════════ │
│ │
┌────────▼─────────────┐ ┌──────────▼───────────────┐
│ PlayerConnection │ │ RemotePlayerConnection │
│ (真实连接) │ │ (虚拟连接) │
│ 或模拟连接 │ │ │
└────────┬─────────────┘ └──────────┬───────────────┘
│ │
└──────────────┬───────────────┘
┌──────▼──────────────────────────────┐
│ RemoteDebuggerInRuntime │
│ (MonoBehaviour) │
│ ┌──────────────────────────────┐ │
│ │ _sampleOnce (bool) │ │
│ │ _autoSample (bool) │ │
│ └──────────────────────────────┘ │
│ ┌──────────────────────────────┐ │
│ │ Awake() - 初始化连接 │ │
│ │ OnEnable() - 注册回调 │ │
│ │ LateUpdate() - 采样触发 │ │
│ │ OnHandleEditorMessage() - 收命令│ │
│ └──────────────────────────────┘ │
└──────────┬──────────────────────────┘
┌──────▼────────────────────┐
│ YooAssets.GetDebugReport() │
│ (全系统数据收集入口) │
└──────┬────────────────────┘
┌───────────┼───────────┐
│ │ │
┌──────────▼──┐ ┌─────▼────┐ ┌──▼─────────────┐
│ResourcePkg 1│ │ResourcePkg2 │ResourcePackageN│
└──────┬──────┘ └────┬─────┘ └──┬──────────┘
│ │ │
└───────────────┼──────────┘
┌───────────▼──────────┐
│ DebugPackageData │
│ ┌─────────────────┐ │
│ │ PackageName │ │
│ │ ProviderInfos[] │ │
│ │ BundleInfos[] │ │
│ │ OperationInfos[]│ │
│ └─────────────────┘ │
└─────────────────────┘
```
---
## 注意事项
1. **DEBUG 模式自动启用**
- 诊断系统仅在 `DEBUG` 模式下启用(通过 `#if DEBUG` 条件编译)
- Release 构建中不会包含诊断代码,无性能开销
2. **版本兼容性**
- 编辑器和运行时的调试器版本必须一致
- 版本不一致的数据会被自动丢弃
3. **历史数据限制**
- 最多缓存 500 帧历史数据(可配置)
- 超过限制后,最早的数据会被移除
4. **JSON 序列化深度限制**
- Unity JsonUtility 序列化深度限制为 10 层
- 操作树Childs嵌套过深可能导致序列化失败
5. **性能开销**
- 单次采样:低开销,仅在需要时采集
- 自动采样:每帧采集,有一定性能开销,建议仅在需要时开启
6. **LateUpdate 时机**
- 采样在 LateUpdate 中执行,确保该帧所有操作已更新
- 避免在采样过程中资源状态发生变化
7. **非序列化字典**
- `DebugPackageData.BundleInfoDic` 使用 `[NonSerialized]` 标记
- 字典在首次查询时才构建,减少序列化开销
8. **场景追踪条件编译**
- `SpawnScene` 字段仅在 DEBUG 模式下赋值(`[Conditional("DEBUG")]`
- Release 构建中该字段为空字符串
---
## 性能优化建议
1. **按需采样**
- 优先使用单次采样Sample Once
- 仅在需要连续监控时开启自动采样Record
2. **及时关闭 Record**
- 分析完成后及时关闭自动采样
- 避免不必要的性能开销
3. **合理设置历史缓存**
- 根据内存情况调整 `MaxReportCount`
- 默认 500 帧已足够大多数分析场景
4. **导出数据离线分析**
- 对于复杂的性能问题,导出 JSON 数据
- 在编辑器外使用专业工具进行分析
5. **Release 构建移除诊断代码**
- 确保 Release 构建使用 Release 配置
- 诊断代码通过 `#if DEBUG` 自动移除

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5fa2b66c20800124c8dd5cb77e854ce3
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e9d6cb1ce5d510645866ad7c122abfab
guid: bb70e2274ce5a5d419cfbd7212efdf4a
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,110 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 下载后台
/// </summary>
/// <remarks>
/// 基于 Unity 内置 UnityWebRequest 实现的下载后台。
/// 支持自定义 UnityWebRequest 创建方式,例如添加证书验证、代理设置等。
/// </remarks>
internal sealed class UnityWebRequestBackend : IDownloadBackend
{
private readonly UnityWebRequestCreator _webRequestCreator;
/// <summary>
/// 后端名称
/// </summary>
public string Name
{
get { return nameof(UnityWebRequestBackend); }
}
/// <summary>
/// 创建 UnityWebRequest 下载后端(使用默认创建方式)
/// </summary>
public UnityWebRequestBackend() : this(null)
{
}
/// <summary>
/// 创建 UnityWebRequest 下载后端
/// </summary>
/// <param name="webRequestCreator">
/// 自定义 UnityWebRequest 创建委托(可选)。
/// 如果为 null则使用默认的 UnityWebRequest 构造方式。
/// </param>
public UnityWebRequestBackend(UnityWebRequestCreator webRequestCreator)
{
_webRequestCreator = webRequestCreator;
}
/// <summary>
/// 驱动更新
/// </summary>
/// <remarks>
/// UnityWebRequest 由 Unity 引擎自动驱动,无需额外更新。
/// </remarks>
public void Update()
{
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
// 无需释放资源
}
/// <summary>
/// 创建 HEAD 请求
/// </summary>
public IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestHeadDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建文件下载请求
/// </summary>
public IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args)
{
return new UnityWebRequestFileDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建字节下载请求
/// </summary>
public IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestBytesDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建文本下载请求
/// </summary>
public IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args)
{
return new UnityWebRequestTextDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建 AssetBundle 下载请求
/// </summary>
public IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args)
{
return new UnityWebRequestAssetBundleDownloader(args, _webRequestCreator);
}
/// <summary>
/// 创建模拟下载请求
/// </summary>
public IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args)
{
return new VirtualFileDownloader(args);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 932e4c4574244dac8c27d9ddc156f8ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,10 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
/// <summary>
/// 自定义下载器的请求委托
/// </summary>
public delegate UnityWebRequest UnityWebRequestCreator(string url, string method);
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0fa3e6346decc4d4db3b03e6ae93e57a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1d30af9086de4fb282c2cdd3d26244c0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,90 @@
using System;
using UnityEngine;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest AssetBundle 下载器
/// </summary>
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包。
/// 支持 Unity 内置缓存机制和 CRC 校验。
/// </remarks>
internal sealed class UnityWebRequestAssetBundleDownloader : UnityWebRequestDownloaderBase, IDownloadAssetBundleRequest
{
private readonly DownloadAssetBundleRequestArgs _args;
private DownloadHandlerAssetBundle _downloadHandler;
/// <summary>
/// 下载结果AssetBundle 对象)
/// </summary>
public AssetBundle Result { get; private set; }
/// <summary>
/// 构造 AssetBundle 下载器
/// </summary>
/// <param name="args">AssetBundle 下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestAssetBundleDownloader(DownloadAssetBundleRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
_downloadHandler = CreateAssetBundleDownloadHandler();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = _downloadHandler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
AssetBundle assetBundle = _downloadHandler.assetBundle;
if (assetBundle == null)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] URL: {URL} - AssetBundle object is null";
}
else
{
Result = assetBundle;
}
}
/// <summary>
/// 创建 AssetBundle 下载处理器
/// </summary>
private DownloadHandlerAssetBundle CreateAssetBundleDownloadHandler()
{
DownloadHandlerAssetBundle handler;
if (_args.DisableUnityWebCache)
{
// 禁用 Unity 缓存
handler = new DownloadHandlerAssetBundle(URL, _args.UnityCRC);
}
else
{
if (string.IsNullOrEmpty(_args.FileHash))
throw new YooInternalException("File hash is null or empty !");
// 使用 Unity 缓存
// 说明The file hash defining the version of the asset bundle.
Hash128 fileHash = Hash128.Parse(_args.FileHash);
handler = new DownloadHandlerAssetBundle(URL, fileHash, _args.UnityCRC);
}
return handler;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e01cc308d7179a34281087fafe455b42
guid: 6f635344c08e2b04295a108c2bcc6a40
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,52 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 字节下载器
/// </summary>
/// <remarks>
/// 将下载内容保存到内存中的字节数组。
/// </remarks>
internal sealed class UnityWebRequestBytesDownloader : UnityWebRequestDownloaderBase, IDownloadBytesRequest
{
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 下载结果(字节数组)
/// </summary>
public byte[] Result { get; private set; }
/// <summary>
/// 构造字节数组下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestBytesDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerBuffer();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
Result = _webRequest.downloadHandler.data;
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 6602c4be2ef295546b7bbb328de8fb0c
guid: 38884a96e8c2df74082d4e059e2e73ed
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,288 @@
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 下载器基类
/// </summary>
/// <remarks>
/// 封装 UnityWebRequest 的通用下载逻辑,包括状态管理、进度追踪等。
/// 子类只需实现 CreateWebRequest 方法来创建特定类型的下载请求。
/// </remarks>
internal abstract class UnityWebRequestDownloaderBase : IDownloadRequest
{
private readonly UnityWebRequestCreator _webRequestCreator;
protected UnityWebRequest _webRequest;
// 看门狗相关
private int _watchdogTime = 0;
private bool _watchdogAborted = false;
private long _lastDownloadBytes = -1;
private double _lastGetDataTime;
#region
/// <summary>
/// 请求地址
/// </summary>
public string URL { get; }
/// <summary>
/// 是否完成
/// </summary>
/// <remarks>
/// 每次调用都会主动轮询请求 PollingRequest
/// </remarks>
public bool IsDone
{
get
{
PollingRequest();
return Status == EDownloadRequestStatus.Succeed
|| Status == EDownloadRequestStatus.Failed
|| Status == EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 请求状态
/// </summary>
public EDownloadRequestStatus Status { get; protected set; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { get; private set; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
public long DownloadedBytes { get; private set; }
/// <summary>
/// HTTP 返回码
/// </summary>
public long HttpCode { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; protected set; }
#endregion
/// <summary>
/// 构造下载器基类
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
protected UnityWebRequestDownloaderBase(string url, UnityWebRequestCreator webRequestCreator)
{
URL = url;
_webRequestCreator = webRequestCreator;
Status = EDownloadRequestStatus.None;
}
/// <summary>
/// 发起请求
/// </summary>
public void SendRequest()
{
if (Status == EDownloadRequestStatus.None)
{
Status = EDownloadRequestStatus.Running;
try
{
CreateWebRequest();
if (_webRequest == null)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] Created web request is null.";
}
else
{
_webRequest.SendWebRequest();
}
}
catch (Exception ex)
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] Failed to create web request : {ex.Message}";
}
}
}
/// <summary>
/// 轮询请求
/// </summary>
public void PollingRequest()
{
if (Status != EDownloadRequestStatus.Running)
return;
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
CheckWatchdog();
if (_webRequest.isDone == false)
return;
HttpCode = _webRequest.responseCode;
#if UNITY_2020_3_OR_NEWER
bool isSuccess = _webRequest.result == UnityWebRequest.Result.Success;
#else
bool isSuccess = !_webRequest.isNetworkError && !_webRequest.isHttpError;
#endif
if (isSuccess)
{
Status = EDownloadRequestStatus.Succeed;
OnRequestSucceed();
}
else
{
Status = EDownloadRequestStatus.Failed;
Error = $"[{GetType().Name}] URL: {URL} - 错误: {_webRequest.error}";
OnRequestFailed();
}
// 完成后释放
DisposeWebRequest();
}
/// <summary>
/// 中止请求
/// </summary>
public void AbortRequest()
{
if (Status == EDownloadRequestStatus.None || Status == EDownloadRequestStatus.Running)
{
Status = EDownloadRequestStatus.Aborted;
if (_webRequest != null)
_webRequest.Abort();
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
DisposeWebRequest();
}
/// <summary>
/// 创建 UnityWebRequest子类实现
/// </summary>
protected abstract void CreateWebRequest();
/// <summary>
/// 请求成功时的回调(子类可重写)
/// </summary>
protected virtual void OnRequestSucceed()
{
}
/// <summary>
/// 请求失败时的回调(子类可重写)
/// </summary>
protected virtual void OnRequestFailed()
{
}
/// <summary>
/// 创建 UnityWebRequest GET 请求
/// </summary>
/// <param name="requestUrl">请求地址</param>
/// <returns>UnityWebRequest 实例</returns>
protected UnityWebRequest CreateUnityWebRequestGet(string requestUrl)
{
if (_webRequestCreator != null)
return _webRequestCreator.Invoke(requestUrl, UnityWebRequest.kHttpVerbGET);
return new UnityWebRequest(requestUrl, UnityWebRequest.kHttpVerbGET);
}
/// <summary>
/// 创建 UnityWebRequest HEAD 请求
/// </summary>
/// <param name="requestUrl">请求地址</param>
/// <returns>UnityWebRequest 实例</returns>
protected UnityWebRequest CreateUnityWebRequestHead(string requestUrl)
{
if (_webRequestCreator != null)
return _webRequestCreator.Invoke(requestUrl, UnityWebRequest.kHttpVerbHEAD);
return new UnityWebRequest(requestUrl, UnityWebRequest.kHttpVerbHEAD);
}
/// <summary>
/// 应用通用请求参数
/// </summary>
protected void ApplyRequestOptions(int timeout, int watchdogTime, Dictionary<string, string> headers)
{
if (_webRequest == null)
throw new YooInternalException("Web request is null !");
// 设置看门狗超时时间
_watchdogTime = watchdogTime;
// 设置响应的超时时间
if (timeout > 0)
_webRequest.timeout = timeout;
// 设置响应头
if (headers != null)
{
foreach (var header in headers)
{
_webRequest.SetRequestHeader(header.Key, header.Value);
}
}
}
/// <summary>
/// 检测看门狗
/// </summary>
private void CheckWatchdog()
{
if (_watchdogTime == 0)
return;
if (_watchdogAborted)
return;
double realtimeSinceStartup = TimeUtility.RealtimeSinceStartup;
if (DownloadedBytes != _lastDownloadBytes)
{
_lastDownloadBytes = DownloadedBytes;
_lastGetDataTime = realtimeSinceStartup;
}
else
{
double deltaTime = realtimeSinceStartup - _lastGetDataTime;
if (deltaTime > _watchdogTime)
{
_watchdogAborted = true;
AbortRequest(); //看门狗终止网络请求
}
}
}
/// <summary>
/// 释放资源
/// </summary>
private void DisposeWebRequest()
{
if (_webRequest != null)
{
//注意引擎底层会自动调用Abort方法
_webRequest.Dispose();
_webRequest = null;
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 375d88bcf5b9a6146adaf98ceb5369f8
guid: 76b0712524ed69542914db8e44fa64fd
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,56 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 文件下载器
/// </summary>
/// <remarks>
/// 将下载内容保存到本地文件,支持断点续传和追加写入。
/// </remarks>
internal sealed class UnityWebRequestFileDownloader : UnityWebRequestDownloaderBase, IDownloadFileRequest
{
private readonly DownloadFileRequestArgs _args;
/// <summary>
/// 文件保存路径
/// </summary>
public string SavePath
{
get { return _args.SavePath; }
}
/// <summary>
/// 构造文件下载器
/// </summary>
/// <param name="args">文件下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestFileDownloader(DownloadFileRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerFile(_args.SavePath, _args.AppendToFile);
handler.removeFileOnAbort = _args.RemoveFileOnAbort;
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
// 断点续传:设置 Range 请求头
if (_args.ResumeFromBytes > 0)
{
_webRequest.SetRequestHeader("Range", $"bytes={_args.ResumeFromBytes}-");
}
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b7557eb146572de49a1ec9b3f3c0b706
guid: 101a4be1bb0a85c4d84ecbf3e74f89e4
MonoImporter:
externalObjects: {}
serializedVersion: 2

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest HEAD 请求下载器
/// </summary>
/// <remarks>
/// 仅获取响应头信息,不下载实际内容。
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// </remarks>
internal sealed class UnityWebRequestHeadDownloader : UnityWebRequestDownloaderBase, IDownloadHeadRequest
{
// 注意:缓存响应头(因为 WebRequest 释放后无法获取)
private Dictionary<string, string> _cachedResponseHeaders;
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 获取 ETag 响应头
/// </summary>
public string ETag
{
get { return GetResponseHeader("ETag"); }
}
/// <summary>
/// 获取 Last-Modified 响应头
/// </summary>
public string LastModified
{
get { return GetResponseHeader("Last-Modified"); }
}
/// <summary>
/// 获取 Content-Type 响应头
/// </summary>
public string ContentType
{
get { return GetResponseHeader("Content-Type"); }
}
/// <summary>
/// 获取 Content-Length 响应头
/// 预期下载的总字节数
/// </summary>
public long ContentLength
{
get
{
string contentLengthStr = GetResponseHeader("Content-Length");
if (string.IsNullOrEmpty(contentLengthStr))
return -1;
if (long.TryParse(contentLengthStr, out long contentLength))
{
return contentLength;
}
else
{
return -1;
}
}
}
/// <summary>
/// 构造 HEAD 请求下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestHeadDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="name">响应头名称(不区分大小写)</param>
/// <returns>响应头的值,如果不存在或请求未完成则返回 null</returns>
public string GetResponseHeader(string name)
{
if (_cachedResponseHeaders == null)
return null;
// 注意UnityWebRequest 的响应头 key 是小写的
string lowerName = name.ToLowerInvariant();
if (_cachedResponseHeaders.TryGetValue(lowerName, out string value))
return value;
return null;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
_webRequest = CreateUnityWebRequestHead(URL);
_webRequest.downloadHandler = null; // HEAD 请求不需要 DownloadHandler
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
var headers = _webRequest.GetResponseHeaders();
if (headers != null)
{
_cachedResponseHeaders = new Dictionary<string, string>(headers.Count, StringComparer.OrdinalIgnoreCase);
foreach (var kvp in headers)
{
string name = kvp.Key.ToLowerInvariant();
string value = kvp.Value;
_cachedResponseHeaders[name] = value;
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ed243d707130394aa85e20dee876d8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
using UnityEngine.Networking;
namespace YooAsset
{
/// <summary>
/// UnityWebRequest 文本下载器
/// </summary>
/// <remarks>
/// 将下载内容解析为 UTF-8 文本字符串。
/// </remarks>
internal sealed class UnityWebRequestTextDownloader : UnityWebRequestDownloaderBase, IDownloadTextRequest
{
private readonly DownloadDataRequestArgs _args;
/// <summary>
/// 下载结果(文本字符串)
/// </summary>
public string Result { get; private set; }
/// <summary>
/// 构造文本下载器
/// </summary>
/// <param name="args">数据下载参数</param>
/// <param name="webRequestCreator">UnityWebRequest 创建器(可选)</param>
public UnityWebRequestTextDownloader(DownloadDataRequestArgs args, UnityWebRequestCreator webRequestCreator)
: base(args.URL, webRequestCreator)
{
_args = args;
}
/// <summary>
/// 创建 UnityWebRequest
/// </summary>
protected override void CreateWebRequest()
{
var handler = new DownloadHandlerBuffer();
_webRequest = CreateUnityWebRequestGet(URL);
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
ApplyRequestOptions(_args.Timeout, _args.WatchdogTime, _args.Headers);
}
/// <summary>
/// 请求成功时的回调
/// </summary>
protected override void OnRequestSucceed()
{
Result = _webRequest.downloadHandler.text;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9b0bd3024ca5bae4dbdc05c7af7479df
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,141 @@
using System;
namespace YooAsset
{
/// <summary>
/// 模拟下载器
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// 根据配置的下载速度模拟进度变化。
/// </remarks>
internal sealed class VirtualFileDownloader : IDownloadFileRequest
{
private readonly DownloadSimulateRequestArgs _args;
private double _lastUpdateTime;
/// <summary>
/// 文件保存路径(模拟下载不需要)
/// </summary>
public string SavePath
{
get { return null; }
}
#region
/// <summary>
/// 请求地址
/// </summary>
public string URL { get; }
/// <summary>
/// 是否完成
/// </summary>
public bool IsDone
{
get
{
PollingRequest();
return Status == EDownloadRequestStatus.Succeed
|| Status == EDownloadRequestStatus.Failed
|| Status == EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 请求状态
/// </summary>
public EDownloadRequestStatus Status { get; private set; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { get; private set; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
public long DownloadedBytes { get; private set; }
/// <summary>
/// HTTP 返回码(模拟固定返回 200
/// </summary>
public long HttpCode { get; private set; }
/// <summary>
/// 错误信息
/// </summary>
public string Error { get; private set; }
#endregion
/// <summary>
/// 构造模拟下载器
/// </summary>
/// <param name="args">模拟下载参数</param>
public VirtualFileDownloader(DownloadSimulateRequestArgs args)
{
_args = args;
URL = args.URL;
Status = EDownloadRequestStatus.None;
}
/// <summary>
/// 发起请求
/// </summary>
public void SendRequest()
{
if (Status == EDownloadRequestStatus.None)
{
Status = EDownloadRequestStatus.Running;
_lastUpdateTime = TimeUtility.RealtimeSinceStartup;
}
}
/// <summary>
/// 轮询请求
/// </summary>
public void PollingRequest()
{
if (Status != EDownloadRequestStatus.Running)
return;
double currentTime = TimeUtility.RealtimeSinceStartup;
double deltaTime = currentTime - _lastUpdateTime;
_lastUpdateTime = currentTime;
// 计算本帧下载的字节数
long downloadBytes = (long)(_args.DownloadSpeed * deltaTime);
DownloadedBytes += downloadBytes;
if (_args.FileSize > 0)
DownloadProgress = (float)DownloadedBytes / _args.FileSize;
// 检查是否完成
if (DownloadedBytes >= _args.FileSize)
{
HttpCode = 200;
DownloadProgress = 1f;
DownloadedBytes = _args.FileSize;
Status = EDownloadRequestStatus.Succeed;
}
}
/// <summary>
/// 中止请求
/// </summary>
public void AbortRequest()
{
if (Status == EDownloadRequestStatus.None || Status == EDownloadRequestStatus.Running)
{
Status = EDownloadRequestStatus.Aborted;
}
}
/// <summary>
/// 释放资源
/// </summary>
public void Dispose()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 84daeb1559aadff40b5b5aa5c81d7d64
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,118 +0,0 @@

namespace YooAsset
{
/// <summary>
/// 下载器结束
/// </summary>
public struct DownloaderFinishData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 是否成功
/// </summary>
public bool Succeed;
}
/// <summary>
/// 下载器相关的更新数据
/// </summary>
public struct DownloadUpdateData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载进度 (0-1f)
/// </summary>
public float Progress;
/// <summary>
/// 下载文件总数
/// </summary>
public int TotalDownloadCount;
/// <summary>
/// 当前完成的下载文件数量
/// </summary>
public int CurrentDownloadCount;
/// <summary>
/// 下载数据总大小(单位:字节)
/// </summary>
public long TotalDownloadBytes;
/// <summary>
/// 当前完成的下载数据大小(单位:字节)
/// </summary>
public long CurrentDownloadBytes;
}
/// <summary>
/// 下载器相关的错误数据
/// </summary>
public struct DownloadErrorData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载失败的文件名称
/// </summary>
public string FileName;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorInfo;
}
/// <summary>
/// 下载器相关的文件数据
/// </summary>
public struct DownloadFileData
{
/// <summary>
/// 所属包裹名称
/// </summary>
public string PackageName;
/// <summary>
/// 下载的文件名称
/// </summary>
public string FileName;
/// <summary>
/// 下载的文件大小
/// </summary>
public long FileSize;
}
/// <summary>
/// 导入文件的信息
/// </summary>
public struct ImportFileInfo
{
/// <summary>
/// 本地文件路径
/// </summary>
public string FilePath;
/// <summary>
/// 资源包名称
/// </summary>
public string BundleName;
/// <summary>
/// 资源包GUID
/// </summary>
public string BundleGUID;
}
}

View File

@@ -0,0 +1,349 @@
using System.Collections.Generic;
namespace YooAsset
{
/// <summary>
/// 导入的资源包信息
/// </summary>
public struct ImportBundleInfo
{
/// <summary>
/// 本地文件路径
/// </summary>
public string FilePath;
/// <summary>
/// 资源包名称
/// </summary>
public string BundleName;
/// <summary>
/// 资源包GUID
/// </summary>
public string BundleGUID;
}
/// <summary>
/// 下载请求状态
/// </summary>
internal enum EDownloadRequestStatus
{
/// <summary>
/// 未开始
/// </summary>
None,
/// <summary>
/// 进行中
/// </summary>
Running,
/// <summary>
/// 已成功
/// </summary>
Succeed,
/// <summary>
/// 已失败
/// </summary>
Failed,
/// <summary>
/// 已中止
/// </summary>
Aborted,
}
/// <summary>
/// 文件下载请求参数
/// </summary>
/// <remarks>
/// 用于将下载内容保存到本地文件的请求配置。
/// 支持断点续传和追加写入模式。
/// </remarks>
internal struct DownloadFileRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 文件保存路径
/// </summary>
public readonly string SavePath;
/// <summary>
/// 是否追加写入文件
/// </summary>
/// <remarks>
/// 配合 ResumeFromBytes 使用,用于断点续传场景。
/// </remarks>
public readonly bool AppendToFile;
/// <summary>
/// 中止请求时是否删除目标文件
/// </summary>
public readonly bool RemoveFileOnAbort;
/// <summary>
/// 断点续传的起始字节(小于等于 0 表示不启用)
/// </summary>
/// <remarks>
/// 推荐由后端自动设置 Range 请求头:"bytes={ResumeFromBytes}-"。
/// </remarks>
public readonly long ResumeFromBytes;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造文件下载请求参数
/// </summary>
public DownloadFileRequestArgs(
string url,
string savePath,
int timeout,
int watchdogTime,
bool appendToFile = false,
bool removeFileOnAbort = true,
long resumeFromBytes = 0)
{
URL = url;
SavePath = savePath;
Timeout = timeout;
WatchdogTime = watchdogTime;
AppendToFile = appendToFile;
RemoveFileOnAbort = removeFileOnAbort;
ResumeFromBytes = resumeFromBytes;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// 数据下载请求参数(通用)
/// </summary>
/// <remarks>
/// 用于下载到内存的请求配置。
/// 可用于字节数组bytes或文本text下载。
/// </remarks>
internal struct DownloadDataRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造数据下载请求参数
/// </summary>
/// <param name="url">请求地址</param>
/// <param name="options">通用请求参数</param>
public DownloadDataRequestArgs(string url, int timeout, int watchdogTime)
{
URL = url;
Timeout = timeout;
WatchdogTime = watchdogTime;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// AssetBundle 下载请求参数
/// </summary>
/// <remarks>
/// 用于下载并加载 Unity AssetBundle 的请求配置。
/// 支持 Unity 内置缓存机制和 CRC 校验。
/// </remarks>
internal struct DownloadAssetBundleRequestArgs
{
/// <summary>
/// 请求地址
/// </summary>
public readonly string URL;
/// <summary>
/// 响应的超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 当 Timeout 设置为 0 时,不应用超时。
/// 设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </remarks>
public readonly int Timeout;
/// <summary>
/// 看门狗超时时间(单位:秒)
/// </summary>
/// <remarks>
/// 用于监控下载任务的数据接收情况。
/// 规则说明:
/// 1. 当设置值为 0 时,表示禁用看门狗监控。
/// 2. 每次接收到下载数据时,看门狗计时器会重置。
/// 3. 若在设定的时间范围内未收到任何数据,任务将被自动终止。
/// </remarks>
public readonly int WatchdogTime;
/// <summary>
/// 禁用 Unity 的网络缓存
/// </summary>
public readonly bool DisableUnityWebCache;
/// <summary>
/// AssetBundle 文件哈希(用于 UnityWebRequest 的缓存)
/// </summary>
/// <remarks>
/// 仅当 DisableUnityWebCache 为 false 时需要。
/// </remarks>
public readonly string FileHash;
/// <summary>
/// Unity CRC 校验值
/// </summary>
public readonly uint UnityCRC;
/// <summary>
/// 自定义请求头(可选)
/// </summary>
public Dictionary<string, string> Headers;
/// <summary>
/// 构造 AssetBundle 下载请求参数
/// </summary>
public DownloadAssetBundleRequestArgs(
string url,
int timeout,
int watchdogTime,
bool disableUnityWebCache = true,
string fileHash = null,
uint unityCrc = 0)
{
URL = url;
Timeout = timeout;
WatchdogTime = watchdogTime;
DisableUnityWebCache = disableUnityWebCache;
FileHash = fileHash;
UnityCRC = unityCrc;
Headers = null;
}
/// <summary>
/// 添加请求头数据
/// </summary>
public void AddRequestHeader(string name, string value)
{
if (Headers == null)
Headers = new Dictionary<string, string>(10);
Headers.Add(name, value);
}
}
/// <summary>
/// 模拟下载请求参数
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// </remarks>
internal struct DownloadSimulateRequestArgs
{
/// <summary>
/// 请求地址(仅用于标识)
/// </summary>
public readonly string URL;
/// <summary>
/// 模拟的文件大小(字节)
/// </summary>
public readonly long FileSize;
/// <summary>
/// 模拟的下载速度(字节/秒)
/// </summary>
/// <remarks>
/// 用于计算模拟的下载进度。
/// </remarks>
public readonly long DownloadSpeed;
/// <summary>
/// 构造模拟下载请求参数
/// </summary>
/// <param name="url">请求地址(仅用于标识)</param>
/// <param name="fileSize">模拟的文件大小(字节)</param>
/// <param name="downloadSpeed">模拟的下载速度(字节/秒),默认 1MB/s</param>
public DownloadSimulateRequestArgs(string url, long fileSize, long downloadSpeed = 1024 * 1024)
{
URL = url;
FileSize = fileSize;
DownloadSpeed = downloadSpeed > 0 ? downloadSpeed : 1024 * 1024;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eb0480e877454f7c944f66a01646b6d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -3,32 +3,8 @@ using UnityEngine;
namespace YooAsset
{
/// <summary>
/// 自定义下载器的请求委托
/// </summary>
public delegate UnityWebRequest UnityWebRequestDelegate(string url);
internal class DownloadSystemHelper
{
#if UNITY_EDITOR
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void OnRuntimeInitialize()
{
UnityWebRequestCreater = null;
}
#endif
public static UnityWebRequestDelegate UnityWebRequestCreater = null;
public static UnityWebRequest NewUnityWebRequestGet(string requestURL)
{
UnityWebRequest webRequest;
if (UnityWebRequestCreater != null)
webRequest = UnityWebRequestCreater.Invoke(requestURL);
else
webRequest = new UnityWebRequest(requestURL, UnityWebRequest.kHttpVerbGET);
return webRequest;
}
/// <summary>
/// 获取WWW加载本地资源的路径
/// </summary>
@@ -81,7 +57,7 @@ namespace YooAsset
#elif UNITY_STANDALONE_LINUX
url = StringUtility.Format("file:///root/{0}", path);
#else
throw new System.NotImplementedException();
throw new System.NotSupportedException($"[{nameof(DownloadSystemHelper.ConvertToWWWPath)}] not implemented platform: {UnityEngine.Application.platform}");
#endif
// For some special cases when users have special characters in their devices, url paths can not be identified correctly.
@@ -94,12 +70,16 @@ namespace YooAsset
public static bool IsRequestLocalFile(string url)
{
//TODO UNITY_STANDALONE_OSX平台目前无法确定
// 本地文件传输协议
if (url.StartsWith("file:"))
return true;
// JAR文件协议
if (url.StartsWith("jar:file:"))
return true;
return false;
}
}
}
}

View File

@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: e7f5776546411834d9ed949d54a6f241
guid: d724672f4d6f24b4db435d10eef6c40d
folderAsset: yes
DefaultImporter:
externalObjects: {}

View File

@@ -0,0 +1,78 @@
using System;
namespace YooAsset
{
/// <summary>
/// 下载后台接口
/// </summary>
/// <remarks>
/// 不同网络库UnityWebRequest / BestHTTP / 自研)实现该接口,用于创建具体下载请求。
/// 每个后台实例是独立的,不共享全局状态。
/// </remarks>
internal interface IDownloadBackend : IDisposable
{
/// <summary>
/// 后台名称(用于日志与调试)
/// </summary>
string Name { get; }
/// <summary>
/// 驱动更新
/// </summary>
/// <remarks>
/// 部分第三方网络库需要在 Unity 主线程中周期性调用 Update 进行驱动。
/// 不需要驱动的后台可实现为空方法。
/// </remarks>
void Update();
/// <summary>
/// 创建 HEAD 请求
/// </summary>
/// <remarks>
/// 仅获取响应头信息,不下载实际内容。
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// </remarks>
/// <param name="args">数据请求参数</param>
/// <returns>HEAD 请求实例</returns>
IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建文件下载请求
/// </summary>
/// <param name="args">文件下载参数</param>
/// <returns>文件下载请求实例</returns>
IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args);
/// <summary>
/// 创建内存下载请求(字节数组)
/// </summary>
/// <param name="args">数据下载参数</param>
/// <returns>字节下载请求实例</returns>
IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建文本下载请求
/// </summary>
/// <param name="args">数据下载参数</param>
/// <returns>文本下载请求实例</returns>
IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args);
/// <summary>
/// 创建 AssetBundle 下载请求
/// </summary>
/// <param name="args">AssetBundle 下载参数</param>
/// <returns>AssetBundle 下载请求实例</returns>
IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args);
/// <summary>
/// 创建模拟下载请求
/// </summary>
/// <remarks>
/// 用于编辑器模式下模拟下载进度,不进行实际网络请求。
/// 可用于测试下载流程和 UI 展示。
/// </remarks>
/// <param name="args">模拟下载参数</param>
/// <returns>模拟下载请求实例</returns>
IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e88c026ebbfb40258aad52ad1c2feb70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,194 @@
using System;
namespace YooAsset
{
/// <summary>
/// 可轮询的下载请求接口
/// </summary>
/// <remarks>
/// 上层通常会在每帧轮询 Status IsDone并在完成后读取结果。
/// </remarks>
internal interface IDownloadRequest : IDisposable
{
/// <summary>
/// 请求地址
/// </summary>
string URL { get; }
/// <summary>
/// 是否完成(成功/失败/中止)
/// </summary>
bool IsDone { get; }
/// <summary>
/// 请求状态
/// </summary>
EDownloadRequestStatus Status { get; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
/// <remarks>
/// 部分情况下无法准确获取总长度,可返回 0。
/// </remarks>
float DownloadProgress { get; }
/// <summary>
/// 当前请求已接收的字节数
/// </summary>
/// <remarks>
/// 断点续传场景下,该值仅表示"本次请求新增下载的字节数",不包含已存在的本地文件长度。
/// </remarks>
long DownloadedBytes { get; }
/// <summary>
/// HTTP 返回码
/// </summary>
/// <remarks>
/// 非 HTTP 协议可返回 0。使用 long 类型以兼容各种协议的返回码。
/// </remarks>
long HttpCode { get; }
/// <summary>
/// 错误信息
/// </summary>
/// <remarks>
/// 失败时不为空。
/// </remarks>
string Error { get; }
/// <summary>
/// 发起请求
/// </summary>
void SendRequest();
/// <summary>
/// 轮询请求
/// </summary>
void PollingRequest();
/// <summary>
/// 中止请求
/// </summary>
void AbortRequest();
}
/// <summary>
/// HEAD 请求接口(仅获取响应头)
/// </summary>
/// <remarks>
/// 用于检查资源是否存在、获取资源大小、检查缓存有效性等场景。
/// 不下载实际内容,仅获取响应头信息。
/// </remarks>
internal interface IDownloadHeadRequest : IDownloadRequest
{
/// <summary>
/// 获取 ETag 响应头
/// </summary>
/// <remarks>
/// 用于缓存验证,如果服务器未返回则为 null。
/// </remarks>
string ETag { get; }
/// <summary>
/// 获取 Last-Modified 响应头
/// </summary>
/// <remarks>
/// 资源最后修改时间,如果服务器未返回则为 null。
/// </remarks>
string LastModified { get; }
/// <summary>
/// 获取 Content-Type 响应头
/// </summary>
/// <remarks>
/// 资源的 MIME 类型,如果服务器未返回则为 null。
/// </remarks>
string ContentType { get; }
/// <summary>
/// 预期下载的总字节数Content-Length
/// </summary>
/// <remarks>
/// 从响应头 Content-Length 获取。
/// 如果服务器未返回或请求未完成,返回 -1。
/// 用于更准确的进度计算。
/// </remarks>
long ContentLength { get; }
/// <summary>
/// 获取响应头信息
/// </summary>
/// <param name="name">响应头名称(不区分大小写)</param>
/// <returns>响应头的值,如果不存在或请求未完成则返回 null</returns>
/// <remarks>
/// 常用响应头Content-Length、Content-Type、ETag、Last-Modified 等。
/// </remarks>
string GetResponseHeader(string name);
}
/// <summary>
/// 文件下载请求接口
/// </summary>
/// <remarks>
/// 将下载内容保存到指定的本地文件路径。
/// </remarks>
internal interface IDownloadFileRequest : IDownloadRequest
{
/// <summary>
/// 文件保存路径
/// </summary>
string SavePath { get; }
}
/// <summary>
/// 内存下载请求接口(字节数组)
/// </summary>
/// <remarks>
/// 将下载内容保存到内存中的字节数组。
/// </remarks>
internal interface IDownloadBytesRequest : IDownloadRequest
{
/// <summary>
/// 下载结果(字节数组)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
byte[] Result { get; }
}
/// <summary>
/// 文本下载请求接口
/// </summary>
/// <remarks>
/// 将下载内容解析为 UTF-8 文本字符串。
/// </remarks>
internal interface IDownloadTextRequest : IDownloadRequest
{
/// <summary>
/// 下载结果(文本字符串)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
string Result { get; }
}
/// <summary>
/// AssetBundle 下载请求接口
/// </summary>
/// <remarks>
/// 下载并加载 Unity AssetBundle 资源包。
/// </remarks>
internal interface IDownloadAssetBundleRequest : IDownloadRequest
{
/// <summary>
/// 下载结果AssetBundle 对象)
/// </summary>
/// <remarks>
/// 仅在请求成功时可用,失败时为 null。
/// </remarks>
UnityEngine.AssetBundle Result { get; }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2a2aafeb309243f4959394a97d66b19f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,113 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityAssetBundleRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private DownloadHandlerAssetBundle _downloadhandler;
private readonly PackageBundle _packageBundle;
private readonly bool _disableUnityWebCache;
private ESteps _steps = ESteps.None;
/// <summary>
/// 请求结果
/// </summary>
public AssetBundle Result { private set; get; }
internal UnityAssetBundleRequestOperation(PackageBundle packageBundle, bool disableUnityWebCache, string url) : base(url)
{
_packageBundle = packageBundle;
_disableUnityWebCache = disableUnityWebCache;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
AssetBundle assetBundle = _downloadhandler.assetBundle;
if (assetBundle == null)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler asset bundle object is null !";
}
else
{
_steps = ESteps.Done;
Result = assetBundle;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
_downloadhandler = CreateWebDownloadHandler();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.downloadHandler = _downloadhandler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
private DownloadHandlerAssetBundle CreateWebDownloadHandler()
{
if (_disableUnityWebCache)
{
var downloadhandler = new DownloadHandlerAssetBundle(_requestURL, _packageBundle.UnityCRC);
#if UNITY_2020_3_OR_NEWER
downloadhandler.autoLoadAssetBundle = false;
#endif
return downloadhandler;
}
else
{
// 注意:优先从浏览器缓存里获取文件
// The file hash defining the version of the asset bundle.
Hash128 fileHash = Hash128.Parse(_packageBundle.FileHash);
var downloadhandler = new DownloadHandlerAssetBundle(_requestURL, fileHash, _packageBundle.UnityCRC);
#if UNITY_2020_3_OR_NEWER
downloadhandler.autoLoadAssetBundle = false;
#endif
return downloadhandler;
}
}
}
}

View File

@@ -1,89 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebCacheRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private readonly Dictionary<string, string> _headers = new Dictionary<string, string>();
private ESteps _steps = ESteps.None;
internal UnityWebCacheRequestOperation(string url) : base(url)
{
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
/// <summary>
/// 设置请求头信息
/// </summary>
public void SetRequestHeader(string name, string value)
{
_headers.Add(name, value);
}
private void CreateWebRequest()
{
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.disposeDownloadHandlerOnDispose = true;
// 设置消息头
foreach (var keyValuePair in _headers)
{
string name = keyValuePair.Key;
string value = keyValuePair.Value;
_webRequest.SetRequestHeader(name, value);
}
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

@@ -1,96 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebDataRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
/// <summary>
/// 请求结果
/// </summary>
public byte[] Result { private set; get; }
internal UnityWebDataRequestOperation(string url, int timeout) : base(url)
{
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
var fileData = _webRequest.downloadHandler.data;
if (fileData == null || fileData.Length == 0)
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler data is null or empty !";
}
else
{
_steps = ESteps.Done;
Result = fileData;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerBuffer handler = new DownloadHandlerBuffer();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

@@ -1,83 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebFileRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private readonly string _fileSavePath;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
internal UnityWebFileRequestOperation(string url, string fileSavePath, int timeout) : base(url)
{
_fileSavePath = fileSavePath;
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
_steps = ESteps.Done;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerFile handler = new DownloadHandlerFile(_fileSavePath);
handler.removeFileOnAbort = true;
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 2ab5f2486d06e2642ba7aa9c9623430e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,98 +0,0 @@
using System;
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal abstract class UnityWebRequestOperation : AsyncOperationBase
{
protected UnityWebRequest _webRequest;
protected readonly string _requestURL;
private bool _isAbort = false;
/// <summary>
/// HTTP返回码
/// </summary>
public long HttpCode { private set; get; }
/// <summary>
/// 当前下载的字节数
/// </summary>
public long DownloadedBytes { protected set; get; }
/// <summary>
/// 当前下载进度0f - 1f
/// </summary>
public float DownloadProgress { protected set; get; }
/// <summary>
/// 请求的URL地址
/// </summary>
public string URL
{
get { return _requestURL; }
}
internal UnityWebRequestOperation(string url)
{
_requestURL = url;
}
internal override void InternalAbort()
{
//TODO
// 1. 编辑器下停止运行游戏的时候主动终止下载任务
// 2. 真机上销毁包裹的时候主动终止下载任务
if (_isAbort == false)
{
if (_webRequest != null)
{
_webRequest.Abort();
_isAbort = true;
}
}
}
/// <summary>
/// 释放下载器
/// </summary>
protected void DisposeRequest()
{
if (_webRequest != null)
{
//注意引擎底层会自动调用Abort方法
_webRequest.Dispose();
_webRequest = null;
}
}
/// <summary>
/// 检测请求结果
/// </summary>
protected bool CheckRequestResult()
{
HttpCode = _webRequest.responseCode;
#if UNITY_2020_3_OR_NEWER
if (_webRequest.result != UnityWebRequest.Result.Success)
{
Error = $"URL : {_requestURL} Error : {_webRequest.error}";
return false;
}
else
{
return true;
}
#else
if (_webRequest.isNetworkError || _webRequest.isHttpError)
{
Error = $"URL : {_requestURL} Error : {_webRequest.error}";
return false;
}
else
{
return true;
}
#endif
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 4e5c3c1a1655a8b41b585c6811201583
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,96 +0,0 @@
using UnityEngine.Networking;
using UnityEngine;
namespace YooAsset
{
internal class UnityWebTextRequestOperation : UnityWebRequestOperation
{
protected enum ESteps
{
None,
CreateRequest,
Download,
Done,
}
private UnityWebRequestAsyncOperation _requestOperation;
private ESteps _steps = ESteps.None;
/// <summary>
/// 响应的超时时间单位在经过Timeout的秒数后尝试中止。
/// 注意当Timeout设置为0时不会应用超时。
/// 注意设置的超时值可能应用于Android上的每个URL重定向这可能会导致响应时间增加。
/// </summary>
private readonly int _timeout;
/// <summary>
/// 请求结果
/// </summary>
public string Result { private set; get; }
internal UnityWebTextRequestOperation(string url, int timeout) : base(url)
{
_timeout = timeout;
}
internal override void InternalStart()
{
_steps = ESteps.CreateRequest;
}
internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;
if (_steps == ESteps.CreateRequest)
{
CreateWebRequest();
_steps = ESteps.Download;
}
if (_steps == ESteps.Download)
{
DownloadProgress = _webRequest.downloadProgress;
DownloadedBytes = (long)_webRequest.downloadedBytes;
Progress = _requestOperation.progress;
if (_requestOperation.isDone == false)
return;
if (CheckRequestResult())
{
var fileText = _webRequest.downloadHandler.text;
if (string.IsNullOrEmpty(fileText))
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = $"URL : {_requestURL} Download handler text is null or empty !";
}
else
{
_steps = ESteps.Done;
Result = fileText;
Status = EOperationStatus.Succeed;
}
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
}
// 注意:最终释放请求器
DisposeRequest();
}
}
private void CreateWebRequest()
{
DownloadHandlerBuffer handler = new DownloadHandlerBuffer();
_webRequest = DownloadSystemHelper.NewUnityWebRequestGet(_requestURL);
_webRequest.timeout = _timeout;
_webRequest.downloadHandler = handler;
_webRequest.disposeDownloadHandlerOnDispose = true;
_requestOperation = _webRequest.SendWebRequest();
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a488de5dcd6f4c448a47c4b574d5c9bc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,557 @@
# DownloadSystem 下载模块
## 模块概述
DownloadSystem 是 YooAsset 资源管理系统的**底层网络下载层**,负责处理所有 HTTP 网络请求。该模块提供了统一的下载接口抽象,支持文件下载、断点续传、并发请求(由上层调度)、看门狗监控等功能。
### 核心职责
- HTTP/HTTPS 文件下载
- 断点续传支持
- 看门狗超时保护
- 多种下载类型(文件/字节/文本/AssetBundle
- 可插拔的网络库后端
---
## 边界与上层协作
DownloadSystem 的职责是提供“可替换后端 + 统一请求接口 + 轮询式生命周期”的基础能力:
- **本模块不负责并发队列/限流调度**:并发通常由上层同时创建多个 request 并自行控制并发数。
- **本模块不负责重试/回退策略**:失败后的重试、切换 CDN、降级等策略通常由上层系统实现。
- **本模块不负责持久化下载任务**:断点续传依赖本地已有文件与 `Range` 请求头,并由上层管理断点信息。
---
## 设计目标
| 目标 | 说明 |
|------|------|
| **可扩展性** | 支持可插拔的网络库后端UnityWebRequest/BestHTTP/自研) |
| **鲁棒性** | 看门狗超时保护、自动清理失败文件、完整错误信息 |
| **高性能** | 轮询模式无阻塞、支持并发请求(并发数由上层调度) |
| **易用性** | 流畅的参数构建 API、清晰的状态转换 |
---
## 架构概念
### 分层架构
```
┌─────────────────────────────────────────────────────────┐
│ 上层调用者 │
│ (FileSystem / ResourceManager) │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ IDownloadBackend │
│ (后端接口) │
│ 定义网络库合约,工厂模式创建请求 │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ IDownloadRequest │
│ (请求接口) │
│ 轮询式生命周期管理,状态机驱动 │
└─────────────────────────┬───────────────────────────────┘
┌─────────────────────────▼───────────────────────────────┐
│ UnityWebRequest / 其他网络库 │
│ (底层实现) │
└─────────────────────────────────────────────────────────┘
```
### 核心组件
- **后端层 (IDownloadBackend)**: 定义网络库实现合约,通过工厂方法创建各类请求
- **请求层 (IDownloadRequest)**: 统一的请求生命周期管理,支持轮询驱动
- **参数层 (Args 结构体)**: 配置下载行为(超时、断点续传、看门狗等)
---
## 文件结构
```
DownloadSystem/
├── Interface/ # 接口定义
│ ├── IDownloadBackend.cs # 后端接口(工厂模式)
│ └── IDownloadRequest.cs # 请求接口层次结构
├── DefaultDownloadBackend/ # 默认后端实现
│ ├── UnityWebRequestBackend.cs # UnityWebRequest 后端
│ └── UnityWebRequestCreator.cs # UnityWebRequest 创建委托
├── DefaultDownloadRequest/ # 默认请求实现
│ ├── UnityWebRequestDownloaderBase.cs # 基础下载器(抽象类)
│ ├── UnityWebRequestFileDownloader.cs # 文件下载器
│ ├── UnityWebRequestHeadDownloader.cs # HEAD 请求器
│ ├── UnityWebRequestBytesDownloader.cs # 字节下载器
│ ├── UnityWebRequestTextDownloader.cs # 文本下载器
│ ├── UnityWebRequestAssetBundleDownloader.cs # AssetBundle 下载器
│ └── VirtualFileDownloader.cs # 模拟下载器(编辑器用)
├── DownloadSystemDefine.cs # 枚举、结构体定义
├── DownloadSystemHelper.cs # 工具函数
└── WebRequestCounter.cs # 请求失败计数器
```
---
## 接口说明
### IDownloadBackend后端接口
定义网络库实现的合约,通过工厂方法创建各类下载请求。
```csharp
public interface IDownloadBackend
{
/// <summary>
/// 后端标识名称(用于日志/调试)
/// </summary>
string Name { get; }
/// <summary>
/// 定期驱动更新(部分第三方库需要)
/// </summary>
void Update();
// 工厂方法 - 创建各类请求
IDownloadHeadRequest CreateHeadRequest(DownloadDataRequestArgs args);
IDownloadFileRequest CreateFileRequest(DownloadFileRequestArgs args);
IDownloadBytesRequest CreateBytesRequest(DownloadDataRequestArgs args);
IDownloadTextRequest CreateTextRequest(DownloadDataRequestArgs args);
IDownloadAssetBundleRequest CreateAssetBundleRequest(DownloadAssetBundleRequestArgs args);
IDownloadFileRequest CreateSimulateRequest(DownloadSimulateRequestArgs args);
}
```
### IDownloadRequest基础请求接口
所有下载请求的通用接口,定义生命周期和状态管理。
```csharp
public interface IDownloadRequest : IDisposable
{
// 元信息
string URL { get; }
// 生命周期
bool IsDone { get; } // 每次访问自动轮询
EDownloadRequestStatus Status { get; }
// 进度跟踪
float DownloadProgress { get; } // 0f - 1f
long DownloadedBytes { get; } // 本次请求新增字节数
// 诊断信息
long HttpCode { get; }
string Error { get; }
// 生命周期方法
void SendRequest(); // 发起请求
void PollingRequest(); // 轮询状态
void AbortRequest(); // 中止请求
}
```
### 专化请求接口
| 接口 | 用途 | 特有属性 |
|------|------|----------|
| `IDownloadHeadRequest` | HEAD 请求,获取响应头 | `ETag`, `LastModified`, `ContentLength`, `ContentType` |
| `IDownloadFileRequest` | 文件下载到本地 | `SavePath` |
| `IDownloadBytesRequest` | 下载到内存(字节数组) | `byte[] Result` |
| `IDownloadTextRequest` | 下载文本内容 | `string Result` |
| `IDownloadAssetBundleRequest` | 下载并加载 AssetBundle | `AssetBundle Result` |
---
## 结构体定义
### 请求状态枚举
```csharp
public enum EDownloadRequestStatus
{
None, // 未开始
Running, // 进行中
Succeed, // 已成功
Failed, // 已失败
Aborted // 已中止(用户中止或看门狗超时)
}
```
### 请求参数结构体
#### DownloadFileRequestArgs文件下载参数
```csharp
public struct DownloadFileRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时0=无限制
public int WatchdogTime; // 看门狗超时(秒)
public string SavePath; // 文件保存路径
public bool AppendToFile; // 追加写入(断点续传)
public bool RemoveFileOnAbort; // 中止时删除文件
public long ResumeFromBytes; // 断点续传起始位置
public Dictionary<string, string> Headers; // 自定义请求头
}
```
#### DownloadDataRequestArgs数据下载参数
```csharp
public struct DownloadDataRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时(秒)
public int WatchdogTime; // 看门狗超时(秒)
public Dictionary<string, string> Headers; // 自定义请求头
}
```
#### DownloadAssetBundleRequestArgsAssetBundle 下载参数)
```csharp
public struct DownloadAssetBundleRequestArgs
{
public string URL; // 请求地址
public int Timeout; // 响应超时
public int WatchdogTime; // 看门狗超时
public bool DisableUnityWebCache; // 禁用 Unity 缓存(推荐 true
public string FileHash; // 文件哈希(缓存启用时需要)
public uint UnityCRC; // Unity CRC 校验值
public Dictionary<string, string> Headers;
}
```
#### DownloadSimulateRequestArgs模拟下载参数
```csharp
public struct DownloadSimulateRequestArgs
{
public string URL; // 标识符
public long FileSize; // 模拟文件大小
public long DownloadSpeed; // 模拟速度(字节/秒),默认 1MB/s
}
```
---
## 核心类说明
### UnityWebRequestBackend
默认的后端实现,基于 Unity 的 UnityWebRequest API。
**特性:**
- 支持自定义 UnityWebRequest 创建方式(证书验证、代理等)
- 无需手动调用 Update()UnityWebRequest 自动驱动
```csharp
// 自定义 UnityWebRequest 创建(建议通过 backend 构造函数传入)
UnityWebRequestCreator creator = (url, method) =>
{
var request = new UnityWebRequest(url, method);
// 自定义配置...
return request;
};
IDownloadBackend backend = new UnityWebRequestBackend(creator);
```
### UnityWebRequestDownloaderBase
抽象基类,封装所有下载器的通用逻辑。
**职责:**
- 管理请求生命周期和状态转换
- 实现看门狗监控机制
- 追踪下载进度和字节数
- 处理超时和错误
**生命周期:**
```
None ──► SendRequest() ──► Running ──► PollingRequest() ──┬──► Succeed
├──► Failed
└──► Aborted
```
### 具体下载器
| 下载器 | 实现接口 | 使用场景 |
|--------|----------|----------|
| `UnityWebRequestFileDownloader` | `IDownloadFileRequest` | 大文件下载到本地 |
| `UnityWebRequestHeadDownloader` | `IDownloadHeadRequest` | 检查资源信息 |
| `UnityWebRequestBytesDownloader` | `IDownloadBytesRequest` | 小文件内存加载 |
| `UnityWebRequestTextDownloader` | `IDownloadTextRequest` | 文本文件下载 |
| `UnityWebRequestAssetBundleDownloader` | `IDownloadAssetBundleRequest` | AB 包下载加载 |
| `VirtualFileDownloader` | `IDownloadFileRequest` | 编辑器模拟下载 |
---
## 使用示例
### 基础文件下载
```csharp
// 1. 创建后端和请求
IDownloadBackend backend = new UnityWebRequestBackend();
var args = new DownloadFileRequestArgs(
url: "https://example.com/file.zip",
savePath: "/path/to/save/file.zip",
timeout: 30,
watchdogTime: 0);
IDownloadFileRequest request = backend.CreateFileRequest(args);
// 2. 发起并轮询
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
// 可选:显示进度
float progress = request.DownloadProgress;
}
// 3. 检查结果
if (request.Status == EDownloadRequestStatus.Succeed)
{
Debug.Log("下载成功");
}
else
{
Debug.LogError($"下载失败: {request.Error}");
}
// 4. 清理资源
request.Dispose();
```
### 断点续传
```csharp
// 获取已下载的文件大小
long existingFileSize = new FileInfo(savePath).Length;
var args = new DownloadFileRequestArgs(
url: url,
savePath: savePath,
timeout: 30,
watchdogTime: 0,
appendToFile: true, // 追加写入
removeFileOnAbort: false, // 中止时保留文件
resumeFromBytes: existingFileSize); // 断点位置
IDownloadFileRequest request = backend.CreateFileRequest(args);
request.SendRequest();
// ... 轮询等待完成
```
### 看门狗保护
```csharp
var args = new DownloadFileRequestArgs(
url: url,
savePath: path,
timeout: 30,
watchdogTime: 30); // 30秒无数据自动中止
IDownloadFileRequest request = backend.CreateFileRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
// 检查是否因看门狗超时而中止
if (request.Status == EDownloadRequestStatus.Aborted)
{
Debug.LogWarning("下载超时,已自动中止");
}
```
### HEAD 请求获取文件信息
```csharp
var args = new DownloadDataRequestArgs(
url: "https://example.com/file.zip",
timeout: 30,
watchdogTime: 0);
IDownloadHeadRequest request = backend.CreateHeadRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
if (request.Status == EDownloadRequestStatus.Succeed)
{
long fileSize = request.ContentLength;
string etag = request.ETag;
string lastModified = request.LastModified;
Debug.Log($"文件大小: {fileSize}, ETag: {etag}");
}
```
### 下载字节数据
```csharp
var args = new DownloadDataRequestArgs(
url: "https://example.com/data.json",
timeout: 30,
watchdogTime: 0);
IDownloadBytesRequest request = backend.CreateBytesRequest(args);
request.SendRequest();
while (!request.IsDone)
{
await Task.Yield();
}
if (request.Status == EDownloadRequestStatus.Succeed)
{
byte[] data = request.Result;
// 处理数据...
}
```
---
## 设计模式
### 工厂模式
`IDownloadBackend` 作为工厂接口,创建各类下载请求对象:
```
IDownloadBackend
├── CreateHeadRequest() ──► IDownloadHeadRequest
├── CreateFileRequest() ──► IDownloadFileRequest
├── CreateBytesRequest() ──► IDownloadBytesRequest
├── CreateTextRequest() ──► IDownloadTextRequest
├── CreateAssetBundleRequest() ──► IDownloadAssetBundleRequest
└── CreateSimulateRequest() ──► IDownloadFileRequest
```
### 策略模式
通过实现 `IDownloadBackend` 接口,可以替换底层网络库:
```
IDownloadBackend (接口)
├── UnityWebRequestBackend (默认实现)
├── BestHTTPBackend (可扩展)
└── CustomBackend (自定义)
```
### 状态机模式
请求生命周期通过状态机管理:
```
┌──────┐ SendRequest() ┌─────────┐
│ None │ ──────────────────► │ Running │
└──────┘ └────┬────┘
│ PollingRequest()
┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌─────────┐
│ Succeed │ │ Failed │ │ Aborted │
└─────────┘ └──────────┘ └─────────┘
```
### 看门狗模式
监控数据接收,防止网络卡顿导致请求无限等待:
```
每帧轮询 PollingRequest()
├── 收到新数据 ──► 重置计时器
└── 未收到数据 ──► 计时器累加
└── 超过 WatchdogTime ──► AbortRequest()
```
---
## 类继承关系
```
IDownloadRequest (基础接口)
├── IDownloadHeadRequest (HEAD 请求)
├── IDownloadFileRequest (文件下载)
├── IDownloadBytesRequest (字节下载)
├── IDownloadTextRequest (文本下载)
└── IDownloadAssetBundleRequest (AssetBundle 下载)
UnityWebRequestDownloaderBase (抽象基类)
├── UnityWebRequestFileDownloader ──► IDownloadFileRequest
├── UnityWebRequestHeadDownloader ──► IDownloadHeadRequest
├── UnityWebRequestBytesDownloader ──► IDownloadBytesRequest
├── UnityWebRequestTextDownloader ──► IDownloadTextRequest
└── UnityWebRequestAssetBundleDownloader ──► IDownloadAssetBundleRequest
VirtualFileDownloader (独立实现) ──► IDownloadFileRequest
```
---
## 工具类
### DownloadSystemHelper
提供跨平台的工具函数:
| 方法 | 说明 |
|------|------|
| `ConvertToWWWPath()` | 转换本地路径为 WWW 协议 URL |
| `IsRequestLocalFile()` | 判断是否本地文件请求 |
### WebRequestCounter
请求失败计数器,用于诊断统计:
- 线程安全:内部使用 `Dictionary` 且未加锁,约定只在主线程调用;如需多线程统计请在外层加锁或改造实现
- Key 规则:`$"{packageName}_{eventName}"`
- 统计口径:**仅统计网络请求失败**`IDownloadRequest.Status != Succeed` 时记录),不统计内容为空、校验失败、解析失败等业务层失败
```csharp
// 记录失败
WebRequestCounter.RecordRequestFailed(packageName, eventName);
// 查询失败次数
int count = WebRequestCounter.GetRequestFailedCount(packageName, eventName);
```
---
## 注意事项
1. **资源释放**:使用完毕后务必调用 `Dispose()` 释放资源
- `AbortRequest()` 仅用于中止请求与切换状态,不等同于释放资源;无论成功/失败/中止都需要 `Dispose()`
- 推荐使用 `try/finally` 确保释放(尤其是上层可能提前中止的场景)
2. **断点续传**:需要服务器支持 `Range` 请求头和 `206 Partial Content` 响应
- 若服务端不支持 Range 仍返回 200全量内容可能会被追加写入导致文件损坏
3. **看门狗超时**:设置为 0 表示禁用,建议根据网络环境设置合理值
4. **内存下载**`IDownloadBytesRequest` 会将整个响应体加载到内存,不适合大文件
5. **驱动更新**:部分第三方网络库实现的 backend 可能需要每帧调用 `IDownloadBackend.Update()` 进行驱动
6. **中止语义**`Aborted` 可能来自用户主动 `AbortRequest()` 或看门狗超时;中止场景下 `HttpCode/Error` 可能为默认值(例如 0/空)
7. **线程安全**:所有下载请求的创建和轮询应在主线程进行

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 2c65d3ba8da65004b8d36dbeecc6c6be
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,9 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace YooAsset
{
/// <summary>
/// 网络请求失败计数器(诊断用)
/// </summary>
/// <remarks>
/// 线程安全:内部使用 Dictionary 且未加锁,约定只在 Unity 主线程调用。
/// 如需在多线程/回调线程调用,请在外层加锁或改为并发容器实现。
/// </remarks>
internal class WebRequestCounter
{
#if UNITY_EDITOR
@@ -15,12 +20,12 @@ namespace YooAsset
#endif
/// <summary>
/// 记录网络请求失败事件的次数
/// 失败计数记录表key = $"{packageName}_{eventName}"
/// </summary>
private static readonly Dictionary<string, int> _requestFailedRecorder = new Dictionary<string, int>(1000);
/// <summary>
/// 记录请求失败事件
/// 记录一次失败
/// </summary>
public static void RecordRequestFailed(string packageName, string eventName)
{
@@ -31,14 +36,14 @@ namespace YooAsset
}
/// <summary>
/// 获取请求失败次数
/// 获取失败次数
/// </summary>
public static int GetRequestFailedCount(string packageName, string eventName)
{
string key = $"{packageName}_{eventName}";
if (_requestFailedRecorder.ContainsKey(key) == false)
_requestFailedRecorder.Add(key, 0);
return _requestFailedRecorder[key];
if (_requestFailedRecorder.TryGetValue(key, out int count))
return count;
return 0;
}
}
}
}

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -107,7 +107,7 @@ namespace YooAsset
internal override void InternalWaitForAsyncComplete()
{
//注意:场景加载不支持异步转同步,为了支持同步加载方法需要实现该方法!
InternalUpdate();
RunOnceExecution();
}
public override void UnSuspendLoad()
{

View File

@@ -108,14 +108,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -109,14 +109,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -88,14 +88,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -113,7 +113,7 @@ namespace YooAsset
internal override void InternalWaitForAsyncComplete()
{
//注意:场景加载不支持异步转同步,为了支持同步加载方法需要实现该方法!
InternalUpdate();
RunOnceExecution();
}
public override void UnSuspendLoad()
{

View File

@@ -100,14 +100,7 @@ namespace YooAsset
}
internal override void InternalWaitForAsyncComplete()
{
while (true)
{
if (ExecuteWhileDone())
{
_steps = ESteps.Done;
break;
}
}
RunBatchExecution();
}
}
}

View File

@@ -1,7 +1,7 @@

namespace YooAsset
{
internal class CatalogDefine
internal class CatalogFileDefine
{
/// <summary>
/// 文件极限大小100MB

Some files were not shown because too many files have changed in this diff Show More