YooAsset的API封装记录

前言

        公司同事最近在用YooAsset去做热更逻辑的处理,我正好使用到他所涉及的部分逻辑,也就沿用YooAsset方案了。我个人使用的过程中,对YooAsset做了额外的封装处理。本篇文章就是我个人封装YooAsset的API的代码记录。如果你觉得我写得有问题,我欢迎你来指出我的错误。

        我要事先说明一件事情,下面给出的代码,我并没有做完全的验证,这是因为这些代码是从我最近的项目中提炼并优化后的产物。因为我最近对应的项目停工了,而我自己还在忙其他的项目。所以我现在没时间去做验证。我深知我自己后面在这热乎劲过去后就不会去做了,所以我还是写了。

环境

Unity2022.3.62f3c1
Windows11
YooAsset 2.3.18
Mini Game 2.3.18
UniTask 2.5.11

项目简述

        我这次经手的项目要发布到多个平台:安卓平台,微信小游戏平台和抖音小游戏平台。所以我这次的封装涉及到多平台的适配。因为YooAsset本身就对小游戏平台有对应的支持即其Sample的Mini Game。因此对于小程序平台,我直接使用Mini Game设定的宏来区分。

YooAsset封装

        在提出主要代码之前,我还要先说明一些额外的操作。这是在我个人使用的过程中遇到的问题,而这没办法用YooAsset提供的接口来解决。首先是在我本地模拟的时候,在WebGL平台下会出现文件丢失的问题。本质是因为我没有事先创建Catalog文件。所以我写了下面的代码来辅助创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
using System;
using System.IO;
using System.Text;
using UnityEngine;
using YooAsset;

namespace Common
{
/// <summary>
/// 生成一个空的 BuildinCatalog.bytes,让 WebServerFileSystem 初始化时不报 404
/// 仿照 YooAsset 内部 CatalogTools.SerializeToBinary 的二进制格式:
/// UInt32 FileSign(0x133C5EE) + UTF8(FileVersion) + UTF8(PackageName) + UTF8(PackageVersion) + Int32(Count=0)
/// </summary>
public static class WebServerEmptyCatalogHelper
{
// 与 YooAsset CatalogDefine 保持一致
private const uint FileSign = 0x133C5EE;
private const string FileVersion = "1.0.0";

/// <summary>
/// 确保指定包名的空 Catalog 文件存在于内置目录下
/// </summary>
/// <param name="packageName">包名,如 "MainPackage"</param>
public static void EnsureEmptyCatalog(string packageName)
{
string builtinRoot = GetBuiltinRoot();
string packageDir = Path.Combine(builtinRoot, packageName);
string catalogPath = Path.Combine(packageDir, "BuildinCatalog.bytes");

// 如果已经存在,跳过(避免覆盖已有的正确 catalog)
if (File.Exists(catalogPath))
{
Debug.Log($"[WebServerEmptyCatalogHelper] Catalog 文件已存在,跳过生成: {catalogPath}");
return;
}

try
{
if (!Directory.Exists(packageDir))
Directory.CreateDirectory(packageDir);

byte[] catalogBytes = BuildEmptyCatalogBinary(packageName);
File.WriteAllBytes(catalogPath, catalogBytes);
Debug.Log($"[WebServerEmptyCatalogHelper] 成功生成空 Catalog: {catalogPath} (0 个内置资源)");
}
catch (Exception e)
{
Debug.LogError($"[WebServerEmptyCatalogHelper] 生成 Catalog 失败: {e.Message}");
}
}

/// <summary>
/// 构造一个 Wrappers 数量为 0 的空 catalog 二进制
/// </summary>
private static byte[] BuildEmptyCatalogBinary(string packageName)
{
using (var ms = new MemoryStream())
using (var writer = new BinaryWriter(ms, Encoding.UTF8))
{
// FileSign
writer.Write(FileSign); // UInt32, little-endian

// FileVersion
WriteUTF8(writer, FileVersion);

// PackageName
WriteUTF8(writer, packageName);

// PackageVersion (填一个占位版本号)
WriteUTF8(writer, "0.0.0");

// Wrapper count = 0
writer.Write(0); // Int32, little-endian

writer.Flush();
return ms.ToArray();
}
}

/// <summary>
/// 写入 YooAsset 风格的 UTF8 字符串: UInt16 长度前缀 + UTF-8 字节
/// </summary>
private static void WriteUTF8(BinaryWriter writer, string value)
{
if (string.IsNullOrEmpty(value))
{
writer.Write((ushort)0);
return;
}

byte[] bytes = Encoding.UTF8.GetBytes(value);
if (bytes.Length > ushort.MaxValue)
throw new FormatException($"WriteUTF8 length cannot be greater than {ushort.MaxValue}");

writer.Write((ushort)bytes.Length);
writer.Write(bytes);
}

/// <summary>
/// 复刻 YooAsset 内置根目录计算逻辑:
/// Application.streamingAssetsPath + DefaultYooFolderName
/// </summary>
private static string GetBuiltinRoot()
{
string defaultFolder = YooAssetSettingsData.GetDefaultYooFolderName();
if (string.IsNullOrEmpty(defaultFolder))
return Application.streamingAssetsPath;
else
return Path.Combine(Application.streamingAssetsPath, defaultFolder);
}
}
}

除此之外,我还遇到了一个问题————无法加载本地已有的包。我的游戏更偏向于单机性质类的,本身用YooAsset就很奇怪。但是为了减少包体大小,YooAsset又是必须的。所以我需求完成这个奇怪的需求。即如果存在本地包,而玩家网络不好的时候,优先加载本地的资源,保证玩家先去玩。我用AI帮助我生成了下面的5个代码块,来实现这个需求。因为其使用了YooAsset内部的代码,所以你要先做一些预先操作。选择一个空文件夹,在Unity中创建一个Assembly Definition Reference,并将其Assembly Definition设定为YooAsset的Assembly Definition。代码块实现如下:

OfflineFirstRequestPackageVersionOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System.IO;

namespace YooAsset
{
/// <summary>
/// 在线优先的包裹版本请求操作
/// 说明:优先联网请求最新版本,网络失败才回退到本地缓存(从清单文件名反推版本)。
/// 这样联网时总能拿到最新版本,断网时也能用上次缓存的版本启动。
/// </summary>
internal class OfflineFirstRequestPackageVersionOperation : FSRequestPackageVersionOperation
{
private enum ESteps
{
None,
RequestRemoteVersion,
LoadLocalVersion,
Done,
}

private readonly DefaultCacheFileSystem _cacheFileSystem;
private readonly bool _appendTimeTicks;
private readonly int _timeout;
private FSRequestPackageVersionOperation _remoteVersionOp;
private ESteps _steps = ESteps.None;

internal OfflineFirstRequestPackageVersionOperation(DefaultCacheFileSystem cacheFileSystem, bool appendTimeTicks, int timeout)
{
_cacheFileSystem = cacheFileSystem;
_appendTimeTicks = appendTimeTicks;
_timeout = timeout;
}

internal override void InternalStart()
{
_steps = ESteps.RequestRemoteVersion;
}

internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;

if (_steps == ESteps.RequestRemoteVersion)
{
if (_remoteVersionOp == null)
{
_remoteVersionOp = _cacheFileSystem.RequestPackageVersionAsync(_appendTimeTicks, _timeout);
_remoteVersionOp.StartOperation();
AddChildOperation(_remoteVersionOp);
}

_remoteVersionOp.UpdateOperation();
Progress = _remoteVersionOp.Progress;
if (_remoteVersionOp.IsDone == false)
return;

if (_remoteVersionOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.Done;
PackageVersion = _remoteVersionOp.PackageVersion;
Status = EOperationStatus.Succeed;
}
else
{
// 网络请求失败,回退本地缓存
YooLogger.Warning($"Request remote version failed : {_remoteVersionOp.Error}, fallback to local.");
_steps = ESteps.LoadLocalVersion;
}
}

if (_steps == ESteps.LoadLocalVersion)
{
// 遍历清单目录,寻找任意已缓存的清单文件,反推其版本号
string localVersion = FindCachedVersion();
if (string.IsNullOrEmpty(localVersion) == false)
{
_steps = ESteps.Done;
PackageVersion = localVersion;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = "Request remote version failed and no cached version found.";
}
}
}

internal override string InternalGetDesc()
{
return $"OfflineFirst request version : {_cacheFileSystem.PackageName}";
}

/// <summary>
/// 从本地缓存的清单文件名中反推版本号
/// 清单文件名规则:{package}_{version}.bytes
/// </summary>
private string FindCachedVersion()
{
string manifestRoot = _cacheFileSystem.GetCacheManifestFilesRoot();
if (Directory.Exists(manifestRoot) == false)
return string.Empty;

string prefix = _cacheFileSystem.PackageName + "_";
string[] files = Directory.GetFiles(manifestRoot, prefix + "*.bytes", SearchOption.TopDirectoryOnly);
if (files == null || files.Length == 0)
return string.Empty;

// 取第一个匹配文件,解析出版本号
string fileName = Path.GetFileName(files[0]);
string version = fileName.Substring(prefix.Length);
version = version.Substring(0, version.Length - ".bytes".Length);
return version;
}
}
}

OfflineFirstLoadPackageManifestOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
using System.IO;

namespace YooAsset
{
/// <summary>
/// 在线优先的包裹清单加载操作
/// 说明:优先联网下载最新清单(含哈希校验),网络失败才回退到本地缓存清单。
/// 这样联网时总能更新到最新清单,断网时也能用上次缓存的清单启动。
/// </summary>
internal class OfflineFirstLoadPackageManifestOperation : FSLoadPackageManifestOperation
{
private enum ESteps
{
None,
LoadRemoteManifest,
LoadLocalManifest,
Done,
}

private readonly DefaultCacheFileSystem _cacheFileSystem;
private readonly string _packageVersion;
private readonly int _timeout;
private LoadCachePackageManifestOperation _localLoadOp;
private FSLoadPackageManifestOperation _remoteLoadOp;
private ESteps _steps = ESteps.None;

internal OfflineFirstLoadPackageManifestOperation(DefaultCacheFileSystem cacheFileSystem, string packageVersion, int timeout)
{
_cacheFileSystem = cacheFileSystem;
_packageVersion = packageVersion;
_timeout = timeout;
}

internal override void InternalStart()
{
_steps = ESteps.LoadRemoteManifest;
}

internal override void InternalUpdate()
{
if (_steps == ESteps.None || _steps == ESteps.Done)
return;

if (_steps == ESteps.LoadRemoteManifest)
{
if (_remoteLoadOp == null)
{
_remoteLoadOp = _cacheFileSystem.LoadPackageManifestAsync(_packageVersion, _timeout);
_remoteLoadOp.StartOperation();
AddChildOperation(_remoteLoadOp);
}

_remoteLoadOp.UpdateOperation();
Progress = _remoteLoadOp.Progress;
if (_remoteLoadOp.IsDone == false)
return;

if (_remoteLoadOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.Done;
Manifest = _remoteLoadOp.Manifest;
Status = EOperationStatus.Succeed;
}
else
{
// 网络下载失败,回退本地缓存
YooLogger.Warning($"Load remote manifest failed : {_remoteLoadOp.Error}, fallback to local.");
_steps = ESteps.LoadLocalManifest;
}
}

if (_steps == ESteps.LoadLocalManifest)
{
if (_localLoadOp == null)
{
// 读取本地缓存的哈希文件
string packageHash = ReadLocalPackageHash();
_localLoadOp = new LoadCachePackageManifestOperation(_cacheFileSystem, _packageVersion, packageHash);
_localLoadOp.StartOperation();
AddChildOperation(_localLoadOp);
}

_localLoadOp.UpdateOperation();
Progress = _localLoadOp.Progress;
if (_localLoadOp.IsDone == false)
return;

if (_localLoadOp.Status == EOperationStatus.Succeed)
{
_steps = ESteps.Done;
Manifest = _localLoadOp.Manifest;
Status = EOperationStatus.Succeed;
}
else
{
_steps = ESteps.Done;
Status = EOperationStatus.Failed;
Error = _localLoadOp.Error;
}
}
}

internal override string InternalGetDesc()
{
return $"OfflineFirst load manifest : {_packageVersion}";
}

/// <summary>
/// 读取本地缓存的包裹哈希文件内容
/// </summary>
private string ReadLocalPackageHash()
{
string hashFilePath = _cacheFileSystem.GetCachePackageHashFilePath(_packageVersion);
if (File.Exists(hashFilePath) == false)
return string.Empty;

string content = FileUtility.ReadAllText(hashFilePath);
return string.IsNullOrEmpty(content) ? string.Empty : content;
}
}
}

OfflineFirstFileSystemFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using YooAsset;

namespace YooAssetExtend
{
/// <summary>
/// 离线优先文件系统的公开工厂
/// 说明:业务代码在 Assembly-CSharp 程序集,无法直接访问 internal 的 OfflineFirstFileSystem,
/// 因此通过本工厂类生成 FileSystemParameters,由 YooAsset 内部反射创建。
/// </summary>
public static class OfflineFirstFileSystemFactory
{
/// <summary>
/// 创建离线优先文件系统的参数
/// </summary>
/// <param name="remoteServices">远端资源地址查询服务(首次联网下载时使用)</param>
/// <param name="packageRoot">清单/缓存根目录,传 null 使用 YooAsset 默认缓存目录</param>
/// <returns>文件系统参数,作为主文件系统(放在参数列表最后)传入 InitializeAsync</returns>
public static FileSystemParameters CreateFileSystemParameters(IRemoteServices remoteServices, string packageRoot = null)
{
// 注意:类型必须编译进 YooAsset 程序集(通过 YooAssetExtend.asmref 挂载)
string fileSystemClass = "YooAsset.OfflineFirstFileSystem, YooAsset";
var fileSystemParams = new FileSystemParameters(fileSystemClass, packageRoot);
fileSystemParams.AddParameter(FileSystemParametersDefine.REMOTE_SERVICES, remoteServices);
return fileSystemParams;
}
}
}

OfflineFirstInitializeOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
namespace YooAsset
{
/// <summary>
/// 离线优先文件系统的初始化操作
/// 说明:直接复用缓存文件系统的初始化逻辑(搜索/校验缓存文件、创建下载中心)
/// </summary>
internal class OfflineFirstInitializeOperation : FSInitializeFileSystemOperation
{
private readonly OfflineFirstFileSystem _fileSystem;
private readonly FSInitializeFileSystemOperation _cacheInitOp;

internal OfflineFirstInitializeOperation(OfflineFirstFileSystem fileSystem, FSInitializeFileSystemOperation cacheInitOp)
{
_fileSystem = fileSystem;
_cacheInitOp = cacheInitOp;
}

internal override void InternalStart()
{
_cacheInitOp.StartOperation();
AddChildOperation(_cacheInitOp);
}

internal override void InternalUpdate()
{
_cacheInitOp.UpdateOperation();
Progress = _cacheInitOp.Progress;
if (_cacheInitOp.IsDone == false)
return;

if (_cacheInitOp.Status == EOperationStatus.Succeed)
{
Status = EOperationStatus.Succeed;
}
else
{
Status = EOperationStatus.Failed;
Error = _cacheInitOp.Error;
}
}

internal override string InternalGetDesc()
{
return $"OfflineFirstFileSystem initialize : {_fileSystem.PackageName}";
}
}
}

OfflineFirstInitializeOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
namespace YooAsset
{
/// <summary>
/// 离线优先文件系统
/// 说明:用于"首次联网下载资源,之后断网也能启动"的场景。
/// 本文件系统作为主文件系统(FileSystems 列表最后一个),负责离线读取【版本】和【清单】,
/// 其余 bundle 的加载/下载/缓存全部委托给内部的 DefaultCacheFileSystem。
/// 注意:本类通过 YooAssetExtend.asmref 编译进 YooAsset 程序集,可访问 internal 类型。
/// </summary>
internal class OfflineFirstFileSystem : IFileSystem
{
/// <summary>
/// 缓存文件系统引用,用于复用其清单/哈希文件路径与清单恢复服务
/// </summary>
private readonly DefaultCacheFileSystem _cacheFileSystem;

private string _packageRoot;

/// <summary>
/// 自定义参数:清单根目录(与缓存文件系统的清单目录一致)
/// 首次联网时由业务侧将版本/哈希/清单文件写入此目录
/// </summary>
public string PackageRoot { private set; get; }


public string PackageName { private set; get; }

public string FileRoot
{
get { return _packageRoot; }
}

public int FileCount
{
get { return _cacheFileSystem != null ? _cacheFileSystem.FileCount : 0; }
}


public OfflineFirstFileSystem()
{
_cacheFileSystem = new DefaultCacheFileSystem();
}


#region IFileSystem 实现
public FSInitializeFileSystemOperation InitializeFileSystemAsync()
{
// 初始化交给缓存文件系统,本文件系统本身无异步初始化需求
var operation = new OfflineFirstInitializeOperation(this, _cacheFileSystem.InitializeFileSystemAsync());
return operation;
}

public FSLoadPackageManifestOperation LoadPackageManifestAsync(string packageVersion, int timeout)
{
return new OfflineFirstLoadPackageManifestOperation(_cacheFileSystem, packageVersion, timeout);
}

public FSRequestPackageVersionOperation RequestPackageVersionAsync(bool appendTimeTicks, int timeout)
{
return new OfflineFirstRequestPackageVersionOperation(_cacheFileSystem, appendTimeTicks, timeout);
}

public FSClearCacheFilesOperation ClearCacheFilesAsync(PackageManifest manifest, ClearCacheFilesOptions options)
{
return _cacheFileSystem.ClearCacheFilesAsync(manifest, options);
}

public FSDownloadFileOperation DownloadFileAsync(PackageBundle bundle, DownloadFileOptions options)
{
return _cacheFileSystem.DownloadFileAsync(bundle, options);
}

public FSLoadBundleOperation LoadBundleFile(PackageBundle bundle)
{
return _cacheFileSystem.LoadBundleFile(bundle);
}

public void SetParameter(string name, object value)
{
_cacheFileSystem.SetParameter(name, value);
}

public void OnCreate(string packageName, string packageRoot)
{
PackageName = packageName;
PackageRoot = packageRoot;
_cacheFileSystem.OnCreate(packageName, packageRoot);
_packageRoot = _cacheFileSystem.FileRoot;
}

public void OnDestroy()
{
_cacheFileSystem.OnDestroy();
}

// 关键:本文件系统作为主文件系统(列表最后一个),必须 Belong 返回 true 才能被 GetBelongFileSystem 命中,
// 否则 bundle 会找不到归属文件系统。bundle 相关的所有操作全部委托给内部缓存文件系统处理。
public bool Belong(PackageBundle bundle)
{
return _cacheFileSystem.Belong(bundle);
}
public bool Exists(PackageBundle bundle)
{
return _cacheFileSystem.Exists(bundle);
}
public bool NeedDownload(PackageBundle bundle)
{
return _cacheFileSystem.NeedDownload(bundle);
}
public bool NeedUnpack(PackageBundle bundle)
{
return _cacheFileSystem.NeedUnpack(bundle);
}
public bool NeedImport(PackageBundle bundle)
{
return _cacheFileSystem.NeedImport(bundle);
}
public string GetBundleFilePath(PackageBundle bundle)
{
return _cacheFileSystem.GetBundleFilePath(bundle);
}
public byte[] ReadBundleFileData(PackageBundle bundle)
{
return _cacheFileSystem.ReadBundleFileData(bundle);
}
public string ReadBundleFileText(PackageBundle bundle)
{
return _cacheFileSystem.ReadBundleFileText(bundle);
}
#endregion
}
}

接下来就是主要实现。

YooAssetRemoteServices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using YooAsset;

namespace Common
{
public class YooAssetRemoteServices : IRemoteServices
{
private readonly string _defaultHostServer;
private readonly string _fallbackHostServer;

public YooAssetRemoteServices(string defaultHostServer, string fallbackHostServer)
{
_defaultHostServer = defaultHostServer;
_fallbackHostServer = fallbackHostServer;
}
string IRemoteServices.GetRemoteMainURL(string fileName)
{
return $"{_defaultHostServer}/{fileName}";
}
string IRemoteServices.GetRemoteFallbackURL(string fileName)
{
return $"{_fallbackHostServer}/{fileName}";
}
}
}

YooAssetServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
using Common.Data;
using Cysharp.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using YooAsset;

namespace Common
{
public class YooAssetServer
{
#if UNITY_WEBGL
private static string PackageUrl = "https://....";
private static string BackUpPackageUrl = "https://....";
#else
private static string PackageUrl = "https://....";
private static string BackUpPackageUrl = "https://....";
#endif

private Dictionary<string, ResourcePackage> loadedPackages = new Dictionary<string, ResourcePackage>();

public YooAssetServer()
{
if (!YooAssets.Initialized)
YooAssets.Initialize();
}

// 防止new的时候初始化YooAsset不成功。
public void Init()
{
if (!YooAssets.Initialized)
YooAssets.Initialize();
}

#if UNITY_EDITOR

public bool TestLoad { private get; set; }

private async UniTask<bool> InitializeLocalPackage(string packageName)
{
var package = YooAssets.TryGetPackage(packageName);
if (package == null)
{
package = YooAssets.CreatePackage(packageName);
}

InitializationOperation initializationOperation = null;

var buildResult = EditorSimulateModeHelper.SimulateBuild(packageName);
var packageRoot = buildResult.PackageRootDirectory;
var createParameters = new EditorSimulateModeParameters();
createParameters.EditorFileSystemParameters =
FileSystemParameters.CreateDefaultEditorFileSystemParameters(packageRoot);
initializationOperation = package.InitializeAsync(createParameters);

await initializationOperation.ToUniTask();

if (initializationOperation.Status != EOperationStatus.Succeed)
{
Debug.Log("初始化资源包失败");
return false;
}

// 获取资源版本
var getVersion = package.RequestPackageVersionAsync();
await getVersion.ToUniTask();

if (getVersion.Status != EOperationStatus.Succeed)
{
Debug.Log("请求资源版本失败");
return false;
}
var packageVersion = getVersion.PackageVersion;

// 获取资源清单

var getManifest = package.UpdatePackageManifestAsync(packageVersion);
await getManifest.ToUniTask();

if (getManifest.Status != EOperationStatus.Succeed)
{
Debug.Log("更新资源清单失败");
return false;
}

// 包体加载好后正式载入
loadedPackages[packageName] = package;
return true;
}

#endif

// 初始化包
private async UniTask<bool> RemotePackageInitialize(string packagesName)
{
bool ans = true;

// 注意:不能用Path.Combine拼接URL,Windows下会变成反斜杠
var url = PackageUrl.TrimEnd('/') + "/" + packagesName;
var backUpUrl = BackUpPackageUrl.TrimEnd('/') + "/" + packagesName;
var packageCacheUrl = Path.Combine(PlatformDataUtility.FullUserDataPath, packagesName);

var remoteServices = new YooAssetRemoteServices(url, backUpUrl);


#if UNITY_EDITOR

var package = YooAssets.CreatePackage(packagesName);
InitializeParameters createParameters;

#if UNITY_WEBGL
// WebGL平台必须使用WebPlayMode
{
// 先确保空 catalog 文件存在,避免 WebServerFileSystem 初始化时报 404
WebServerEmptyCatalogHelper.EnsureEmptyCatalog(packagesName);

var webParams = new WebPlayModeParameters();
var webServerFileSystemParams = FileSystemParameters.CreateDefaultWebServerFileSystemParameters();
var webRemoteFileSystemParams = FileSystemParameters.CreateDefaultWebRemoteFileSystemParameters(remoteServices);
webParams.WebServerFileSystemParameters = webServerFileSystemParams;
webParams.WebRemoteFileSystemParameters = webRemoteFileSystemParams;
createParameters = webParams;
}
#else
// 非WebGL平台使用HostPlayMode测试远程下载+本地缓存
{
var hostParams = new HostPlayModeParameters();
var buildinFileSystemParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters();
buildinFileSystemParams.AddParameter(FileSystemParametersDefine.DISABLE_CATALOG_FILE, true);
var cacheFileSystemParams = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices, null, packageCacheUrl);
hostParams.BuildinFileSystemParameters = buildinFileSystemParams;
hostParams.CacheFileSystemParameters = cacheFileSystemParams;
createParameters = hostParams;
}
#endif

var initializationOperation = package.InitializeAsync(createParameters);

await initializationOperation.ToUniTask();

if (initializationOperation.Status != EOperationStatus.Succeed)
{
Debug.LogError($"{packagesName}初始化失败");
ans = false;
}
else
{
Debug.Log($"{packagesName}初始化成功");
await PackageUpdate(package);
}

#else

var package = YooAssets.TryGetPackage(packagesName);
if(package == null)
package = YooAssets.CreatePackage(packagesName);
InitializeParameters createParameters;
#if DOUYINMINIGAME // 抖音
var parameters = new WebPlayModeParameters();
parameters.WebServerFileSystemParameters = TiktokFileSystemCreater.CreateFileSystemParameters(packageCacheUrl, remoteServices);
createParameters = parameters;

#elif WEIXINMINIGAME // 微信
var parameters = new WebPlayModeParameters();
parameters.WebServerFileSystemParameters = WechatFileSystemCreater.CreateFileSystemParameters(packageCacheUrl, remoteServices);
createParameters = parameters;

#else // 现在来说就是安卓平台

var parameters = new HostPlayModeParameters();
parameters.BuildinFileSystemParameters = null;
parameters.CacheFileSystemParameters = OfflineFirstFileSystemFactory.CreateFileSystemParameters(remoteServices, packageCacheUrl);
createParameters = parameters;
#endif

var initializationOperation = package.InitializeAsync(createParameters);

await initializationOperation.ToUniTask();

if (initializationOperation.Status != EOperationStatus.Succeed)
{
ans = false;
}
else
{
await PackageUpdate(package);
}
#endif

// 如果成功就正式赋值,否则移除key
if (ans)
loadedPackages[packagesName] = YooAssets.GetPackage(packagesName);
else loadedPackages.Remove(packagesName);

return ans;
}

public async UniTask<bool> InitializePackage(string packagesName)
{
if (loadedPackages.ContainsKey(packagesName))
{
if (loadedPackages[packagesName] != null)
{
Debug.LogError("包体正在加载,请勿重复初始化");
return false;
}
else return true;
}

// 视为正在加载
loadedPackages[packagesName] = null;

#if UNITY_EDITOR
// 本地编辑器测试远程加载
if (TestLoad)
return await RemotePackageInitialize(packagesName);
return await InitializeLocalPackage(packagesName);
#else
return await RemotePackageInitialize(packagesName);
#endif
}

// 判断包体存在或者正在载入
public bool ExistOrLoadingPackage(string packageName)
{
return loadedPackages.ContainsKey(packageName);
}

// 判断包体是否载入成功
public bool IsLoadedPackage(string packageName)
{
return loadedPackages.ContainsKey(packageName) && loadedPackages[packageName] != null;
}

// 获取包体
public ResourcePackage GetPackage(string packageName)
{
if (loadedPackages.ContainsKey(packageName))
{
return loadedPackages[packageName];
}
return null;
}

// 获取资源的Handle
public AssetHandle LoadRes<T>(string packageName,string url) where T : Object
{
var package = GetPackage(packageName);
if (package == null)
return null;

T obj = null;

var handle = YooAssets.LoadAssetSync(url, typeof(T));

if (handle.Status == EOperationStatus.Succeed)
{
obj = handle.AssetObject as T;
#if UNITY_EDITOR
if (obj == null)
{
Debug.LogError(handle.AssetObject.GetType());
return null;
}
#endif
}
return handle;
}

public async UniTask<AssetHandle> LoadResAsync<T>(string packageName, string url) where T : Object
{
var package = GetPackage(packageName);
if (package == null)
return null;

T obj = null;

var handle = YooAssets.LoadAssetAsync(url, typeof(T));

await handle.ToUniTask();

if (handle.Status == EOperationStatus.Succeed)
{
obj = handle.AssetObject as T;
#if UNITY_EDITOR
if (obj == null)
{
Debug.LogError(handle.AssetObject.GetType());
return null;
}
#endif
}
return handle;
}

// 移除包内未使用的资源
public IEnumerator UnloadUnuseAssetPackageRes(string packageName)
{
if (loadedPackages.ContainsKey(packageName))
{
var package = loadedPackages[packageName];
if (package != null)
yield return package.UnloadUnusedAssetsAsync();
}
}

// 移除所有包内未使用的资源
public IEnumerator UnloadUnuseAssetAllLoadPackageRes()
{
foreach (var package in loadedPackages.Values)
{
if (package != null)
yield return package.UnloadUnusedAssetsAsync();
}
}

#region 通用的更新流程
private async UniTask<bool> PackageUpdate(ResourcePackage package)
{
var version = await RequestPackageVersion(package); // 请求资源版本
var ans = !string.IsNullOrEmpty(version);
int step = 1;
while (ans && step <= 3)
{
switch (step)
{
case 1: // 更新资源清单
ans = await UpdatePackageManifest(package, version);
break;
case 2: // 检查并下载更新文件
ans = await CheckAndDownloadFiles(package);
break;
case 3: // 先清除之前的
await ClearCacheFiles(package);
break;
}
step++;
}

if (!ans)
{
Debug.Log("更新失败");
}
return ans;
}

private async UniTask<string> RequestPackageVersion(ResourcePackage package)
{
string ans = "";
var operation = package.RequestPackageVersionAsync();
await operation.ToUniTask();

if (operation.Status != EOperationStatus.Succeed)
{
Debug.LogError(package.PackageName + "请求资源版本失败:" + operation.Error);
}
else ans = operation.PackageVersion;

return ans;
}

private async UniTask<bool> UpdatePackageManifest(ResourcePackage package, string packageVersion)
{
var operation = package.UpdatePackageManifestAsync(packageVersion);
await operation.ToUniTask();

if (operation.Status != EOperationStatus.Succeed)
{
Debug.LogError($"{package.PackageName}更新资源清单失败:{operation.Error}");
return false;
}
return true;
}

private async UniTask<bool> CheckAndDownloadFiles(ResourcePackage package)
{
int downloadingMaxNum = 10;
int failedTryAgain = 3;
var _downloader = package.CreateResourceDownloader(downloadingMaxNum, failedTryAgain);

if (_downloader.TotalDownloadCount == 0)
{
Debug.Log("没有文件需要下载");
return true;
}

// 发现更新文件,直接开始下载(不等待用户点击)
int totalDownloadCount = _downloader.TotalDownloadCount;
long totalDownloadBytes = _downloader.TotalDownloadBytes;
Debug.Log($"发现需要下载的文件: {totalDownloadCount}个, 总大小: {totalDownloadBytes}字节");

return await DownloadFiles(_downloader);
}

private async UniTask<bool> DownloadFiles(ResourceDownloaderOperation _downloader)
{
_downloader.DownloadErrorCallback = (errorData) =>
{
Debug.LogError($"下载文件失败: {errorData.FileName}, {errorData.ErrorInfo}");
};

_downloader.BeginDownload();
await _downloader.ToUniTask();

if (_downloader.Status != EOperationStatus.Succeed)
{
return false;
}

return true;
}

private async UniTask ClearCacheFiles(ResourcePackage _package)
{
var operation = _package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles);
await operation.ToUniTask();
}
#endregion

}
}

闲言碎语

        我干到一半就感觉不对劲了,这不如我直接上传到github仓库里。但是我已经写了一部分了,那就算了。