with one click
cocos-creator-bundle
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
| name | cocos-creator-bundle |
| description | Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。 |
| category | gamedev |
| tags | ["cocoscreator","bundle","资源"] |
规则: 把非首屏所需的场景、UI、音频拆入独立 bundle;所有 bundle 共用的基础资源(公共 UI 图集、角色骨架、音效库)单独放一个 shared bundle 先行加载;主包只保留启动脚本和 Loading 界面资源。
为什么: AI 生成项目结构时最常见的做法是"把所有资源都放 resources 目录",结果主包体积随着项目增长无节制膨胀,冷启动时间在中低端安卓机上轻松超过 10 秒。具体误区:把战斗场景的 1024×1024 角色图集和大厅场景的 BGM 全部打进主包——用户可能只是点了一下广告入口,根本不会进战斗场景,却已经把战斗资源全下载了。resources 目录下的所有资源会强制进主包,这是另一个常踩的坑。
怎么做:
resources 或主包;战斗、副本、活动等按功能各建一个 bundle。shared bundle,设置优先级最高,在游戏初始化阶段最先加载。Build 面板检查 「Asset Bundle」标签,确认各 bundle 包含资源列表符合预期。规则: 通过 assetManager.loadBundle 获取 AssetBundle 对象,再用 bundle.load 加载具体资源;资源用完后调用 bundle.release(path, type) 或 bundle.releaseAll() 释放;bundle 本身用 assetManager.removeBundle(bundle) 卸载,两步缺一不可。
为什么: 只调用 bundle.releaseAll() 而不调用 assetManager.removeBundle 是高频错误:资源引用计数归零了,但 assetManager 内部仍缓存着这个 bundle 的实例,下次 loadBundle 同名 bundle 时直接返回缓存对象而不重新下载——如果服务器上 bundle 已更新,客户端却拿到旧的缓存,就会出现内容错误但不报任何错的诡异 bug。反过来,只调用 removeBundle 而不先 releaseAll,bundle 内资源引用计数不归零,纹理内存永远不释放。
怎么做:
// 加载 bundle
assetManager.loadBundle("battle", (err, bundle) => {
if (err) { console.error(err); return; }
// 加载具体资源
bundle.load("prefabs/Enemy", Prefab, (err, prefab) => {
if (err) { console.error(err); return; }
prefab.addRef(); // 手动管理引用计数
this._enemyPrefab = prefab;
});
});
// 离开战斗场景时释放
leaveBattle() {
if (this._enemyPrefab) {
this._enemyPrefab.decRef();
this._enemyPrefab = null;
}
const bundle = assetManager.getBundle("battle");
if (bundle) {
bundle.releaseAll(); // 先释放 bundle 内所有资源引用计数
assetManager.removeBundle(bundle); // 再移除 bundle 缓存
}
}
规则: 被多个 bundle 引用的资源必须放入独立的 shared bundle,不能让同一份资源分别打入各个 bundle;加载使用了跨 bundle 依赖的 bundle 时,必须先确保依赖 bundle 已加载完毕。
为什么: Creator 在打包时如果发现某个资源被多个 bundle 引用,且没有显式归属到共享 bundle,会把该资源分别复制打入每个依赖它的 bundle——1MB 的角色骨架可能被复制进 3 个 bundle,包体直接增加 2MB。更危险的是运行时:battle bundle 里的 Enemy prefab 依赖 shared bundle 里的骨架动画,如果 shared 还没加载完就去 instantiate Enemy,会得到材质丢失、动画报错的"紫块人物",且错误日志只提示"找不到资源 UUID",新手很难定位到是加载顺序问题。
怎么做:
shared bundle。loadBundle("shared") → 完成后再并行加载其他 bundle。build/assets 目录,同一资源 UUID 不应出现在多个 bundle 的 manifest 里。规则: 远程 bundle 的 loadBundle 调用必须传入 version 或开启 MD5 资源名,防止浏览器/系统 HTTP 缓存拿到旧版本;在失败回调中实现有限次重试(建议 3 次,指数退避),超过重试次数后向用户展示网络错误提示或降级到内置版本。
为什么: 不加 version 的远程 bundle 请求会被移动端系统网络层缓存:bundle 更新上线后,部分用户的设备缓存了旧的 manifest.json,导致新增的活动道具贴图请求 404、新 prefab 结构与旧代码不匹配引发崩溃。这类 bug 极难复现——测试人员清过缓存所以没问题,但线上大量用户遭遇。另一个高频问题:单次 loadBundle 失败就直接弹错误框退出游戏,用户因为一次网络波动就被踢出,差评率飙升。
怎么做:
const REMOTE_BASE = "https://cdn.example.com/bundles/";
const BUNDLE_VERSION = "20240601";
function loadRemoteBundle(name: string, retries = 3): Promise<AssetManager.Bundle> {
return new Promise((resolve, reject) => {
assetManager.loadBundle(`${REMOTE_BASE}${name}`, { version: BUNDLE_VERSION },
(err, bundle) => {
if (!err) { resolve(bundle); return; }
if (retries > 0) {
// 指数退避重试
this.scheduleOnce(() => {
loadRemoteBundle(name, retries - 1).then(resolve).catch(reject);
}, (4 - retries) * 1.5);
} else {
// 超过重试次数,降级到本地内置 bundle
const fallback = assetManager.getBundle(name);
fallback ? resolve(fallback) : reject(err);
}
}
);
});
}
规则: 场景切换或关闭功能模块时,立即释放该模块专属 bundle(releaseAll + removeBundle);shared 等全局 bundle 驻留内存;建立 BundleManager 单例统一追踪哪些 bundle 当前已加载,避免重复加载和忘记释放。
为什么: 不建立统一管理机制的项目,最终必然出现"同一个 bundle 被 3 个组件各自 loadBundle 了一次,却没有任何一个组件负责 release"的情况。Creator 的 loadBundle 对已加载的 bundle 会直接返回缓存,所以不会报错,内存只增不减。在一次游戏会话里玩家进出了 10 个不同副本,每个副本加载了各自的 bundle,全部常驻内存,低端机直接被系统杀进程。另一个常见失误:在 onDestroy 里 release,但 onDestroy 晚于场景卸载执行,此时资源已被场景的 autoReleaseAssets 部分释放,release 调用顺序混乱导致引用计数算错。
怎么做:
// BundleManager 单例,统一管理 bundle 生命周期
export class BundleManager {
private static _instance: BundleManager;
static get instance() {
return this._instance || (this._instance = new BundleManager());
}
private _loaded: Map<string, AssetManager.Bundle> = new Map();
async load(name: string, options?: Record<string, any>): Promise<AssetManager.Bundle> {
if (this._loaded.has(name)) return this._loaded.get(name)!;
return new Promise((resolve, reject) => {
assetManager.loadBundle(name, options ?? {}, (err, bundle) => {
if (err) { reject(err); return; }
this._loaded.set(name, bundle);
resolve(bundle);
});
});
}
unload(name: string) {
const bundle = this._loaded.get(name);
if (!bundle) return;
bundle.releaseAll();
assetManager.removeBundle(bundle);
this._loaded.delete(name);
}
}
// 反例 — 所有资源放 resources,加载远程 bundle 不带版本,release 不彻底
@ccclass
export class BattleLoader extends Component {
// ❌ 大量资源堆在 resources 导致主包膨胀
// ❌ 没有统一的 bundle 管理,各模块各自 load
async enterBattle() {
// ❌ 远程 bundle 不带 version,HTTP 缓存会返回旧版本
assetManager.loadBundle("https://cdn.example.com/battle", (err, bundle) => {
bundle.load("Enemy", Prefab, (err, prefab) => {
cc.instantiate(prefab); // ❌ 直接用,没有 addRef
});
});
}
leaveBattle() {
const bundle = assetManager.getBundle("battle");
// ❌ 只调用 releaseAll,没有 removeBundle,bundle 缓存仍在
// ❌ prefab 没有 decRef,引用计数不归零,纹理内存永不释放
bundle?.releaseAll();
}
}
// 正例 — BundleManager 统一管理,版本控制,双步释放
@ccclass
export class BattleLoader extends Component {
private _enemyPrefab: Prefab | null = null;
async enterBattle() {
// ✅ 通过 BundleManager 加载,携带版本号,防 HTTP 缓存
const bundle = await BundleManager.instance.load("battle", {
version: "20240601"
});
bundle.load("Enemy", Prefab, (err, prefab) => {
if (err) { console.error("加载 Enemy prefab 失败:", err); return; }
prefab.addRef(); // ✅ 手动管理引用计数
this._enemyPrefab = prefab;
cc.instantiate(prefab);
});
}
leaveBattle() {
// ✅ 先释放具体资源引用计数
if (this._enemyPrefab) {
this._enemyPrefab.decRef();
this._enemyPrefab = null;
}
// ✅ BundleManager 内部 releaseAll + removeBundle,两步都做
BundleManager.instance.unload("battle");
}
}
// 反例 — 没有 shared bundle,各 bundle 里各自包含了同一份角色骨架
// 在代码层面体现为加载顺序不保证,instantiate 时骨架丢失
async loadDungeon() {
// ❌ dungeon bundle 依赖 character 骨架,但没有确保 shared 先加载
assetManager.loadBundle("dungeon", (err, bundle) => {
bundle.load("Knight", Prefab, (err, prefab) => {
// ❌ 骨架动画资源未就绪,instantiate 后是紫色方块
cc.instantiate(prefab);
});
});
}
// 正例 — shared bundle 先行,再加载功能 bundle
async loadDungeon() {
// ✅ 确保 shared bundle(含骨架、公共图集)已加载
await BundleManager.instance.load("shared");
// ✅ shared 就绪后再加载 dungeon,依赖资源已在内存中
const bundle = await BundleManager.instance.load("dungeon");
bundle.load("Knight", Prefab, (err, prefab) => {
if (err) { console.error(err); return; }
prefab.addRef();
this._knightPrefab = prefab;
cc.instantiate(prefab);
});
}
resources 目录移出,拆入对应功能 bundle,主包体积已检查。shared bundle,不在多个 bundle 中重复打包。loadBundle 调用使用 BundleManager 统一管理,没有散落在各组件里的裸 assetManager.loadBundle。version 参数或开启了 MD5 资源名,防止 HTTP 缓存拿到旧版本。addRef,释放时调用了 decRef,bundle 卸载时同时执行了 releaseAll + removeBundle。