ASSET PIPELINE · HOT UPDATE · OBJECT POOL
ZMAsset Framework
面向商业级 Unity 项目的模块化资源管理与热更新框架
将资源打包、加载、缓存、实例化、回收、热更新与远端寻址收拢进一套稳定管线。业务层只面对统一 API,开发环境和 AssetBundle 运行环境可以使用一致的资源路径与调用方式。
多模块打包 · 多线程热更 · 引用计数 · 大型对象池 · AES 加密
框架介绍
ZMAsset 是一套以模块为边界的 Unity 资源基础设施。它同时覆盖编辑器资源加载、AssetBundle 构建与加载、资源热更新、缓存引用管理及对象池复用,适合大厅与多个子游戏并存的大型项目。
| 项目 | 说明 |
|---|---|
| 核心命名空间 | ZM.ZMAsset |
| 推荐入口 | ZMAsset、ZMAddressableAsset |
| 异步模型 | Cysharp.Threading.Tasks.UniTask |
| 示例 Unity 版本 | 2022.3.62f3c1 |
| 加载模式 | Editor / AssetBundle |
| 热更模式 | NoHot / Hot |
| 平台支持 | Android、iOS、Windows、macOS、WebGL 等 |
核心价值
同一份业务代码可以在 Editor 模式快速迭代,在发布环境切换到 AssetBundle;资源模块、热更清单、下载目录和缓存生命周期由框架统一管理。
核心能力
-
多模块资源管线
Hall、GameItem 或子游戏可以拥有独立的打包配置、Bundle 配置表、热更清单和运行时模块。
-
热更新系统
支持版本检测、差异文件计算、多线程下载、失败处理、更新进度和模块级热更。
-
缓存与对象池
内置对象池、资源缓存与引用计数,实例回收时可恢复 Transform、UI 与特效初始状态。
-
资源加密
构建阶段可启用 AES 加密,运行时从本地或热更目录读取后自动解密加载。
-
远端可寻址加载
通过
ZMAddressableAsset使用资源路径与模块名加载本地或网络资源。 -
可视化构建工具
Unity 菜单提供模块配置、正式包、热更包和输出目录等完整编辑器工作流。
架构概览
业务代码
│
├── ZMAsset 常规资源、实例化、场景、热更新
└── ZMAddressableAsset 模块化远端可寻址资源
│
▼
ResourceManager 缓存、对象池、异步任务、引用计数
├── AssetBundleManager
└── AddressableAssetSystem
│
▼
HotAssetsManager 版本检查、清单对比、多线程下载
│
▼
Editor Assets / StreamingAssets / PersistentData / Remote Server
门面优先
日常业务代码建议只调用 ZMAsset 和 ZMAddressableAsset。ResourceManager、AssetBundleManager、HotAssetsManager 属于框架实现层,除扩展框架外不建议直接依赖。
快速接入
1. 创建全局配置
在 Project 窗口创建 AssetsBundleSettings:
配置文件必须放入某个 Resources 目录,并保持资源名为:
框架通过 Resources.Load<BundleSettings>("AssetsBundleSettings") 获取它。
2. 配置运行模式
| 字段 | 作用 | 开发期建议 |
|---|---|---|
loadAssetType |
Editor 或 AssetBundle 加载模式 |
Editor |
bundleHotType |
是否启用资源热更 | NoHot |
AssetBundleDownLoadUrl |
热更服务器根地址 | 按部署环境填写 |
MAX_THREAD_COUNT |
最大下载线程数 | 按目标平台测试后设置 |
ABSUFFIX |
Bundle 文件后缀 | 默认空字符串 |
bundleEncrypt.isEncrypt |
是否开启 Bundle 加密 | 发布策略决定 |
bundleEncrypt.encryptKey |
AES 密钥 | 不要提交真实生产密钥 |
ZMAssetRootPath |
框架相对 Assets 的路径 |
与实际安装目录一致 |
3. 初始化框架与模块
初始化应在首个资源 API 调用之前完成,并且在应用生命周期中只执行一次。
using Cysharp.Threading.Tasks;
using UnityEngine;
using ZM.ZMAsset;
public sealed class GameBootstrap : MonoBehaviour
{
private async void Awake()
{
ZMAsset.InitFrameWork();
bool ready = await ZMAsset.InitAssetsModule(BundleModuleName.Hall);
if (!ready)
{
Debug.LogError("Hall 资源模块初始化失败");
return;
}
Debug.Log("ZMAsset 已就绪");
}
}
初始化顺序
热更项目应先完成目标模块的热更新,再调用 InitAssetsModule 建立 Bundle 配置映射,最后开始加载该模块资源。
4. 定义统一路径
建议集中维护以 Assets/ 开头的完整资源路径:
public static class AssetsPathConfig
{
public const string GameData = "Assets/GameData/";
public const string HallPrefabs = GameData + "Hall/Prefabs/";
public const string HallTextures = GameData + "Hall/Textures/";
}
常用示例
同步实例化与释放
GameObject window = ZMAsset.InstantiateObject(
AssetsPathConfig.HallPrefabs + "MainWindow.prefab",
uiRoot
);
// 默认进入对象池;destroy: true 会销毁对应缓存。
ZMAsset.Release(window);
UniTask 异步实例化
AssetsRequest request = await ZMAsset.InstantiateObjectAsync(
AssetsPathConfig.HallPrefabs + "RankItem.prefab",
content,
param1: rankId
);
GameObject item = request.obj;
int id = (int)request.param1;
// 使用完成后同时归还实例和请求对象。
request.Release();
预热对象池
await ZMAsset.PreLoadObjectAsync<GameObject>(
AssetsPathConfig.HallPrefabs + "NoticeItem.prefab",
count: 20
);
加载常见资源
Sprite icon = await ZMAsset.LoadSpriteAsync(
AssetsPathConfig.HallTextures + "icon_coin.png"
);
Texture texture = await ZMAsset.LoadTextureAsync(
AssetsPathConfig.HallTextures + "background.jpg"
);
AudioClip audio = ZMAsset.LoadAudio("Assets/GameData/Audio/click.wav");
TextAsset config = await ZMAsset.LoadTextAssetAsync("Assets/GameData/Cfg/hero.json");
加载场景
AsyncOperation operation = ZMAsset.LoadSceneAsync(
"Assets/GameData/Battle/Scenes/Battle.unity",
UnityEngine.SceneManagement.LoadSceneMode.Additive
);
Editor 模式下,目标场景需要加入 File → Build Settings → Scenes In Build;AssetBundle 模式不受此限制。
可寻址资源
AssetsRequest request = await ZMAddressableAsset.InstantiateAsyncFormPool(
"Assets/GameData/GameItem/6013/6013.prefab",
parent,
BundleModuleName.AdressAsset
);
Texture texture = await ZMAddressableAsset.LoadTextureAsync(
"Assets/GameData/GameItem/6001/huafei.png",
BundleModuleName.AdressAsset
);
request.Release();
资源热更新
推荐流程
检查更新大小
ZMAsset.CheckAssetsVersion(BundleModuleName.Hall, (needUpdate, sizeMb) =>
{
if (!needUpdate)
{
EnterHall().Forget();
return;
}
updateView.Show($"需要下载 {sizeMb:F2} MB 资源");
});
启动热更新
ZMAsset.HotAssets(
BundleModuleName.Hall,
startHotCallBack: module => Debug.Log($"开始更新:{module}"),
hotFinish: module => OnHotFinished(module).Forget(),
waiteDownLoad: module => Debug.Log($"等待用户确认下载:{module}"),
isCheckAssetsVersion: true
);
private async UniTaskVoid OnHotFinished(string module)
{
if (await ZMAsset.InitAssetsModule(module))
EnterHall();
}
服务器目录约定
运行时代码按照以下结构请求模块清单:
例如:
编辑器构建流程
打开可视化构建窗口:
| 工具/入口 | 用途 |
|---|---|
Bundle Setting |
配置加载、热更、平台、压缩、加密和下载参数 |
| 模块配置 | 定义模块的 Prefab、根目录、指定目录与源目录规则 |
| 正式包构建 | 生成 Bundle、配置表和内嵌资源 |
| 热更包构建 | 生成指定版本差异资源及热更清单 |
ZM/BundleFolder |
打开 AssetBundle 输出目录 |
ZM/HotBundleFolder |
打开热更包目录 |
ZM/PersistentFolder |
打开运行时持久化目录 |
模块打包规则
BundleModuleData 支持多种资源归集方式:
prefabPathArr:以 Prefab 为构建单元。rootFolderPathArr:按根目录子文件夹组织 Bundle。signFolderPathArr:为指定目录配置明确的 Bundle 名称。sourceFolderPathArr:纳入模块的源资源目录。
模块边界
建议按可独立进入、更新或卸载的业务域拆分模块,例如 Hall、Poker、Battle。不要按文件类型把整个项目拆成 Texture、Prefab、Audio 三个超大公共模块。
ZMAsset API
初始化与模块
| API | 返回值 | 说明 |
|---|---|---|
InitFrameWork() |
void |
创建框架实例、回收节点、热更与资源管理器 |
InitAssetsModule(string) |
UniTask<bool> |
初始化指定 AssetBundle 模块 |
CheckAssetsVersion(string, Action<bool,float>) |
void |
返回是否更新及更新体积 |
HotAssets(...) |
void |
启动指定模块热更新流程 |
GetHotAssetsModule(string) |
HotAssetsModule |
获取模块热更状态与高级控制对象 |
实例化与预加载
| API | 返回值 | 使用场景 |
|---|---|---|
InstantiateObject(path, parent) |
GameObject |
同步实例化 |
InstantiateObject(path, parent, position, scale, rotation) |
GameObject |
指定本地变换实例化 |
InstantiateObjectAsync(..., callback, ...) |
void |
回调式异步实例化 |
InstantiateObjectAsync(path, parent, ...) |
UniTask<AssetsRequest> |
推荐的可等待异步实例化 |
InstantiateObjectAndLoad(...) |
long |
边下载边等待实例化,返回任务 ID |
PreLoadObjct(path, count) |
void |
同步预热对象池;API 名按源码拼写 |
PreLoadObjectAsync<T>(path, count) |
UniTask |
异步预热对象池 |
RemoveObjectLoadCallBack(long) |
void |
取消尚未回调的对象加载任务 |
资源加载
| API | 返回值 | 说明 |
|---|---|---|
PreLoadResource<T>(path) |
void |
同步预加载非实例资源 |
PreLoadResourceAsync<T>(path) |
UniTask<T> |
异步预加载资源 |
LoadSprite(path) |
Sprite |
同步加载 Sprite |
LoadSpriteAsync(path) |
UniTask<Sprite> |
异步加载 Sprite;自动补 .png |
LoadSpriteAsync(path, Image, ...) |
long |
加载后直接赋给 Image,返回任务 ID |
LoadTexture(path) |
Texture |
同步加载 Texture;自动补 .jpg |
LoadTextureAsync(path) |
UniTask<Texture> |
异步加载 Texture;自动补 .jpg |
LoadAudio(path) |
AudioClip |
同步加载音频 |
LoadTextAsset(path) |
TextAsset |
同步加载文本资源 |
LoadTextAssetAsync(path) |
UniTask<TextAsset> |
异步加载文本资源 |
LoadScriptableObject<T>(path) |
T |
加载 ScriptableObject |
LoadAtlasSprite(atlasPath, spriteName) |
Sprite |
从 Unity SpriteAtlas 加载 Sprite |
LoadPNGAtlasSprite(atlasPath, spriteName) |
Sprite |
从 TexturePacker 图集加载 Sprite |
LoadSceneAsync(path, mode) |
AsyncOperation |
异步加载场景 |
释放与清理
| API | 说明 |
|---|---|
Release(GameObject, bool destroy = false) |
回收实例;destroy 为 true 时销毁缓存 |
Release(Texture) |
释放 Texture,必须确认没有其他使用方 |
Release(AssetsRequest) |
归还请求对象;通常直接调用 request.Release() |
ClearAllAsyncLoadTask() |
清理所有异步加载任务 |
ClearResourcesAssets(false) |
清理对象池,并按引用计数选择性释放资源 |
ClearResourcesAssets(true) |
深度清理全部框架实例与资源 |
ZMAddressableAsset API
| API | 返回值 | 说明 |
|---|---|---|
InstantiateAsyncFormPool(...) |
UniTask<AssetsRequest> |
从指定可寻址模块实例化对象 |
LoadResourceAsync<T>(path, module) |
UniTask<T> |
加载任意 Unity Object |
PreLoadResourceAsync<T>(path, module) |
UniTask<T> |
预加载模块资源 |
LoadSpriteAsync(path, module) |
UniTask<Sprite> |
加载 Sprite |
LoadTextureAsync(path, module) |
UniTask<Texture> |
加载 Texture |
LoadAudioAsync(path, module) |
UniTask<AudioClip> |
加载 AudioClip |
LoadTextAssetAsync(path, module) |
UniTask<TextAsset> |
加载 TextAsset |
LoadScriptableObject<T>(path, module) |
UniTask<T> |
加载 ScriptableObject |
Release(...) |
void |
复用 ZMAsset 的统一释放流程 |
AssetsRequest
异步实例化返回的请求对象同时承载实例与透传参数:
| 成员 | 类型 | 说明 |
|---|---|---|
obj |
GameObject |
加载并实例化得到的对象 |
param1 |
object |
透传参数 1 |
param2 |
object |
透传参数 2 |
param3 |
object |
透传参数 3 |
Release() |
void |
回收实例、清空参数并归还请求对象 |
建议使用 try/finally 保证释放:
AssetsRequest request = await ZMAsset.InstantiateObjectAsync(path, parent);
try
{
Use(request.obj);
}
finally
{
request?.Release();
}
对象池原始数据
框架通过原始数据组件使对象回收后恢复初始状态。
| 类型 | 恢复内容 | 编辑器入口 |
|---|---|---|
OriginData |
Transform、激活状态、Collider、Rigidbody | Assets/生成原始数据 |
UIOriginData |
RectTransform、锚点、轴心、尺寸、粒子 | Assets/生成UI原始数据 |
EffectOriginData |
Transform、粒子系统、TrailRenderer | Assets/生成特效原始数据 |
选择目标 Prefab 后执行对应菜单,让框架记录初始状态。否则池中对象再次取出时,运行期间修改过的位置、显隐或特效状态可能被保留。
生命周期与内存策略
谁加载,谁释放
将资源所有权绑定到明确的 Window、业务模块或 World 生命周期。不要依赖 ClearResourcesAssets(true) 代替正常释放;深度清理更适合退出子游戏或重置大型业务域。
推荐策略:
- 列表项、飘字和特效等高频对象:预热对象池,使用后
Release(GameObject)。 - 单次异步实例化:持有
AssetsRequest,结束时调用request.Release()。 - 常驻 UI 图集或公共配置:在所属 World 进入时加载,World 退出时统一释放。
- 子游戏切换:先停止异步请求,再执行非深度或深度清理。
Texture:只有明确不存在共享引用时才单独释放。
常见问题
为什么 BundleSettings.Instance 是 null?
确认 AssetsBundleSettings.asset 位于 Resources 目录内,并且文件名没有改变。框架使用固定名称 AssetsBundleSettings 加载配置。
Editor 能加载,打包后找不到资源?
检查资源是否加入正确模块、完整路径大小写是否一致、模块配置表是否随包发布,以及启动阶段是否调用了 InitAssetsModule。
为什么热更完成后仍然读取旧资源?
确认热更完成回调后重新初始化了对应资源模块;同时检查服务器清单平台名、模块名和 Bundle 后缀是否与客户端配置一致。
对象池里的 UI 或特效状态不正确?
为 Prefab 生成对应的 UIOriginData 或 EffectOriginData,并确认新增子节点或组件后重新生成原始数据。
什么时候使用 ZMAddressableAsset?
当资源由独立模块管理、可能位于远端且需要通过模块名寻址时使用。普通内嵌或已初始化 AssetBundle 资源优先使用 ZMAsset。
异步回调已经不需要了怎么办?
保存回调式加载返回的任务 ID,并调用 RemoveObjectLoadCallBack(loadId);切换 World 前也可以调用 ClearAllAsyncLoadTask() 做统一收尾。
发布前检查清单
- [ ]
AssetsBundleSettings.asset位于Resources目录。 - [ ]
ZMAssetRootPath与实际安装路径一致。 - [ ] 所有资源模块名稳定且区分大小写。
- [ ] 目标平台、压缩方式和 Bundle 后缀配置正确。
- [ ] 热更 URL 使用生产 CDN,并验证清单可访问。
- [ ] 加密开启时,构建端和运行端密钥一致。
- [ ] 首包包含必要的模块配置和内嵌 Bundle。
- [ ] 高频 Prefab 已生成正确的 OriginData。
- [ ] 每个加载点都有明确的释放责任方。
- [ ] 真机验证断网、弱网、下载失败和版本回退流程。