TEngine框架学习 热更新和分包下载

本文章都是建立在Winodws平台下测试的,其他平台是否同样适用还未进行验证。
TE框架还在更新中,未来可能有较大变动。

TEngine: 2025.5.20

前言

所有的功能和代码的演示都是基于以下项目演示的,欢迎大家参考。
GitHub: 基于TEngine框架实现的塔防Demo

热更新

TE框架自带了一套完整的热更流程,也有很多优秀的博主做过相关教程,这里的文章以记录为主补充为辅,希望能够帮助到在学习TE框架时遇到问题的读者。

启用热更新

打开安装界面
安装界面
启用热更新
设置工程模拟

参数设置

打开配置文件
修改配置文件

大部分的热更设置都能在UpdateSetting文件中修改,ResDownLoadPath就是资源下载的地址,还有诸如是否强制更新、是否提醒更新这里不做赘述,可以看源码理解一下,这里只涉及配置没有涉及到逻辑功能。

打包

生成热更代码1
生成热更代码2

上面的两步是生成代码的热更文件和插件Dll,运行成功的话再AssetRaw目录下DLL文件夹下应该包含dll文件,如果为空请查看一下热更生成报错。

YooAsset打包
打包方式
资源索引

上面的图一偷一个懒,将工程设置和YooAsset构建放在一起了,相同颜色的方框代表两个选项要保持一致,就如上图,默认包为DefaultPackage,加密方式为File Off Set。

图二展示的是几种打包方式,如果选择可以查看官网教程,这里单独说两种,
None:构建资源但是不做任何操作(StreamingAssets为空)。
ClearAndCopyAll:构建资源并且复制到StreamingAssets下。
前者方便我们单独打更新包或者补充包,后者方便打首包,具体区别可以都试试看。不管怎么样,StreamingAssets下至少应该要包含图三中的文件,本地会用这些文件和服务器中的索引进行比较,没有的话会报错。

测试

可以使用HFS文件服务器部署本地的服务器,如果是默认的配置,将打包生成的所有文件放入LocalServer\yourProjectName\Windows64\xxxx下面`,运行工程即可。

允许Http链接

本地测试需要打开HTTP链接,否则会报错,Unity默认是关闭的。

分包下载

目前框架中的逻辑是一次性下载完全部的资源,这样的坏处就是会造成内存浪费,如果是强制下载再进入游戏的话也会使进入游戏时间过长,体验拉跨。但是在实际开发中,往往会需要我们按需下载,比如只下载下一关、根据玩家选择下载不同的内容,这里就对这个功能进行了部分扩展。

如何分包

YooAsset中将资源分为Package和Asset Tags,下面有对应的总结。博主这里用的是Tags来进行资源管理,主要原因是对于目前正在做的Demo足够了。另一个原因是,如果用Package的话,最理想的方式还需要额外写一套方法,将附加Package的资源索引也当作热更资源,以后遇到使用场景再尝试添加吧。

维度 Package(资源包裹) Asset Tags(资源标签)
本质 构建单元(Build Unit) 逻辑标记(Label / Flag)
作用 隔离资源、独立构建、独立补丁 给资源打关键字,用于运行时筛选
构建结果 生成独立的补丁文件夹(patch) 无独立文件,仅写入清单(manifest)
数量 多个 Package 可并存(如 Main、DLC、MOD) 单个资源可打多个标签(用“;”分隔)
使用场景 模块化打包、DLC/渠道包、并行构建 首包控制、按需下载、版本差异、运行时过滤

资源打包

测试资源Build设置

图一是资源打标签,博主这里添加了三个场景,分别对应Tags,Level3 、4、5,也就是这次需要按需下载的资源,其余的资源全部为Base。

图二是出包的设置,这里选择按标签复制,图上会把Base标签的资源打入StreamingAssets,外部输出的则是所有资源。我们直接将输出的文件丢到服务器上即可。

修改代码

1、脚本IResourceModule中新重载创建下载器函数,第一个接口是默认的,第二个接口是我们新增的,string[] tags就是传入指定Tags的参数。

1
2
3
4
5
6
7
8
9
10
11
/// <summary>
/// 创建资源下载器,用于下载当前资源版本所有的资源包文件。
/// </summary>
/// <param name="customPackageName">指定资源包的名称。不传使用默认资源包</param>
ResourceDownloaderOperation CreateResourceDownloader(string customPackageName = "");

/// <summary>
/// 创建资源下载器,用于下载当前资源版本所有的资源包文件。
/// </summary>
/// <param name="customPackageName">指定资源包的名称。不传使用默认资源包</param>
ResourceDownloaderOperation CreateResourceDownloader(string[] tags, string customPackageName = "");

2、在ResourceModule中实现刚才的接口,另外新增一个判断标签是否存在函数,因为YooAsset本来就能区分标签,只用随便Copy下就行。

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
/// <summary>
/// 创建资源下载器,根据tag下载当前资源版本所有的资源包文件。
/// </summary>
/// <param name="tags"></param>
/// <param name="customPackageName"></param>
/// <returns></returns>
public ResourceDownloaderOperation CreateResourceDownloader(string[] tags, string customPackageName = "")
{
ResourcePackage package = null;
if (string.IsNullOrEmpty(customPackageName))
{
package = YooAssets.GetPackage(this.DefaultPackageName);
}
else
{
package = YooAssets.GetPackage(customPackageName);
}
Downloader = package.CreateResourceDownloader(tags, DownloadingMaxNum, FailedTryAgain);
return Downloader;
}

public bool IsTagResourcesExist(string tag, string packageName = "")
{
var package = string.IsNullOrEmpty(packageName)
? YooAssets.GetPackage(DefaultPackageName)
: YooAssets.GetPackage(packageName);

// 1. 获取标签对应的所有资源包信息
var bundleInfos = package.GetAssetInfo(tag);

if (bundleInfos == null)
{
Log.Warning($"No resources found for tag: {tag}");
return false;
}

return true;
}

3、ProcedureCreateDownloader脚本是游戏启动时检测线上资源的脚本,将CreateResourceDownloader方法传入Base就可以做到启动游戏是判断Base资源是否完整了

1
2
// 换成带参的CreateResourceDownloader方法
_downloader = _resourceModule.CreateResourceDownloader(GameModule.Resource.DefaultTagName);

4、最后就是按需下载资源,这里贴出一个参考代码,博主这边测试是能够运行的,但是很明显代码的保护机制做的不是很完善,最核心的代码就是HandleLevelDownloadAsync函数前面几行,框架里的用法其实大差不多,只是机制更加完整。

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
private async UniTask ShowLevelSelectionButtonItems()
{
int index = 0;
List<UniTask> downloadTasks = new List<UniTask>();

foreach (var itemdata in LevelDataControl.Instance.GetAllLevelData())
{
// 创建子窗口,省略

// 检测并下载对应资源
var task = HandleLevelDownloadAsync(itemdata.GroupName, itemdata.PackageName, itemdata.Id);
downloadTasks.Add(task);

index++;
}

// 等待所有下载任务完成
await UniTask.WhenAll(downloadTasks);
}


/// <summary>
/// 下载level的函数
/// </summary>
/// <param name="packageName"></param>
/// <param name="levelId"></param>
private async UniTask HandleLevelDownloadAsync(string tag, string packageName, int levelId)
{
// 获取并更新远端版本
var versionOp = GameModule.Resource.RequestPackageVersionAsync(false, 60, packageName);
await versionOp.ToUniTask();

//创建下载器
await GameModule.Resource.UpdatePackageManifestAsync(versionOp.PackageVersion, 60, packageName);
var downloader = GameModule.Resource.CreateResourceDownloader(new string[] { tag }, packageName);

if (downloader.TotalDownloadCount > 0)
{
Log.Debug("开始下载关卡资源包: {0} - {1}", packageName, levelId);
downloader.DownloadUpdateCallback = (downloadUpdateData) =>
{
Log.Debug("download update {0}", downloader.Progress);
GameEvent.Send(ChangeSceneEvent.LevelDownloadProgress, new LevelDownloadProgress(levelId, downloader.Progress));
};

downloader.DownloadErrorCallback = (error) =>
{
Log.Error($"下载出错 [{packageName}] : {error}");
GameEvent.Send(ChangeSceneEvent.LevelDownloadProgress, new LevelDownloadProgress(levelId, -1f));
};

downloader.BeginDownload();
await downloader.ToUniTask();

if (downloader.IsDone)
{
GameEvent.Send(ChangeSceneEvent.LevelDownloadProgress, new LevelDownloadProgress(levelId, 1f));
}
else
{
Log.Error($"资源包 {packageName} 下载失败!");
GameEvent.Send(ChangeSceneEvent.LevelDownloadProgress, new LevelDownloadProgress(levelId, -1f));
}
}
else
{
// 已经有资源就直接标记完成
GameEvent.Send(ChangeSceneEvent.LevelDownloadProgress, new LevelDownloadProgress(levelId, 1f));
}
}

TEngine框架学习 热更新和分包下载
https://blog.meo39.com/2025/07/11/TEngineLean3/
作者
daydayasobi
发布于
2025年7月11日
许可协议