원클릭으로
cocos-creator-hotupdate
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
SOC 직업 분류 기준
| name | cocos-creator-hotupdate |
| description | 给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。 |
| category | gamedev |
| tags | ["cocoscreator","热更新","发布"] |
project.manifest / version.manifest,或排查热更新失败/资源不一致问题时。规则: project.manifest 里每个资源条目的 MD5 值必须与服务器上实际文件的 MD5 对应;version.manifest 的版本号在每次发布热更时必须递增;两个 manifest 文件本身不得被 CDN 长期缓存(Cache-Control: no-cache 或极短 TTL)。
为什么: 手写或脚本生成 manifest 时最常见的错误是"构建后忘记重新生成 manifest"——资源 MD5 还是上一次构建的值,与服务器实际文件不匹配。AssetsManager 对比 MD5 时发现一致,认为不需要更新,结果新资源从未被下载,玩家看到的是旧内容但没有任何报错,开发者以为热更成功。另一个陷阱:CDN 把 version.manifest 缓存了 24 小时,服务器上版本号已经更新,客户端拿到的还是旧版本号,不触发更新流程,线上问题持续数小时才被发现。
怎么做:
jsb-link/res 目录的 MD5 计算脚本,每次构建自动生成 manifest,禁止手写。version-generator.js 工具(位于引擎目录),CI/CD 流程在构建后自动调用。project.manifest 和 version.manifest 设置 Cache-Control: no-store 或 max-age=60。curl -I https://cdn.example.com/version.manifest 确认响应头无长期缓存。规则: 必须监听 UPDATE_PROGRESSION(进度)、ALREADY_UP_TO_DATE(已最新)、UPDATE_FINISHED(完成)、ERROR_DOWNLOAD(下载失败)、ERROR_VERIFY(校验失败)等事件,在每个错误事件里记录日志并作出对应处理,不能只监听 UPDATE_FINISHED 就认为热更逻辑完整。
为什么: 只监听成功事件是 AI 生成热更代码时最普遍的问题:assetManager 的事件码有十余个,AI 通常只生成 UPDATE_FINISHED 的处理,其余全部忽略。结果:网络慢导致部分文件下载超时(ERROR_DOWNLOAD)没有触发重试,用户停在进度条 90% 永久卡死;资源下载后文件损坏(ERROR_VERIFY)没有删除损坏文件并重试,下次启动直接崩溃。ERROR_FAILED_DECOMPRESS 在 Android 低版本上尤其常见,忽略它会导致 zip 格式 bundle 无法解压,表现为进入某个功能模块时白屏。
怎么做:
import { native } from "cc";
export class HotUpdateManager {
private _am: native.AssetsManager | null = null;
private _updating = false;
init(manifestPath: string, storagePath: string) {
if (!native.AssetsManager) return; // 非原生平台跳过
this._am = new native.AssetsManager(manifestPath, storagePath);
this._am.setVerifyCallback((filePath, asset) => {
// ✅ 可在此自定义校验逻辑(默认 MD5 校验已够用,返回 true 表示通过)
return true;
});
}
checkUpdate(): Promise<boolean> {
return new Promise((resolve) => {
if (!this._am) { resolve(false); return; }
this._am.setEventCallback((event) => {
const code = event.getEventCode();
const EventCode = native.AssetsManager.EventCode;
if (code === EventCode.ALREADY_UP_TO_DATE) {
resolve(false); // 无需更新
} else if (code === EventCode.NEW_VERSION_FOUND) {
resolve(true); // 有新版本
} else if (code === EventCode.ERROR_DOWNLOAD_MANIFEST ||
code === EventCode.ERROR_PARSE_MANIFEST) {
console.error("manifest 获取失败:", event.getMessage());
resolve(false); // 降级:当做无更新处理
}
});
this._am.checkUpdate();
});
}
update(onProgress: (percent: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
if (!this._am || this._updating) return;
this._updating = true;
const EventCode = native.AssetsManager.EventCode;
this._am.setEventCallback((event) => {
const code = event.getEventCode();
switch (code) {
case EventCode.UPDATE_PROGRESSION:
onProgress(event.getPercent());
break;
case EventCode.UPDATE_FINISHED:
this._updating = false;
resolve();
break;
case EventCode.ERROR_DOWNLOAD:
console.error("下载失败:", event.getAssetId(), event.getMessage());
// ✅ 单个文件下载失败继续其他文件,全部完成后统一重试失败列表
break;
case EventCode.ERROR_VERIFY:
console.error("校验失败:", event.getAssetId());
this._am!.downloadFailedAssets(); // ✅ 重新下载校验失败的文件
break;
case EventCode.ERROR_FAILED_DECOMPRESS:
console.error("解压失败:", event.getMessage());
this._updating = false;
reject(new Error("decompress_failed"));
break;
case EventCode.UPDATE_FAILED:
this._updating = false;
reject(new Error(event.getMessage()));
break;
}
});
this._am.update();
});
}
}
规则: 热更新只用于下发脚本逻辑更新(JS bundle)和小体量资源(配置表、少量贴图);单次热更包不超过总资源的 30%;涉及引擎版本升级、原生插件变更、大规模重构的版本必须走应用商店完整包更新。
为什么: 把热更当成"不想过审就任意更新一切"的通道是应用商店政策红线(iOS App Store 明确禁止通过脚本热更改变核心功能),同时也是工程风险:当单次热更量超过 5MB 时,下载失败率随网络状况急剧上升,更难保证所有用户同步到一致状态。更大的技术风险:热更了 JS 逻辑但原生层(C++ 引擎、JSB 绑定)版本没变,新脚本调用了旧引擎没有的 API,崩溃无法热修复,只能紧急回滚版本号。
怎么做:
project.manifest 的 version 字段里同时记录引擎版本:"engineVersion": "3.8.2",AssetsManager 加载时客户端校验引擎版本是否匹配,不匹配则跳过热更并提示用户更新客户端。/updates/1.0.0/、/updates/1.1.0/,客户端根据自身基础版本号选择对应热更路径。规则: 热更完成后在本地热更目录保留上一版本的备份(或记录上一版本搜索路径快照);热更后首次启动时做完整性自检(关键脚本和 manifest 文件存在且 MD5 正确);自检失败立即删除当前热更目录,切换到上一可用版本路径,不让用户停留在损坏状态。
为什么: 不实现回滚的热更系统是一颗定时炸弹:用户在下载热更过程中断网、存储空间不足、系统强杀进程,都会留下部分写入的损坏文件。下次启动时 AssetsManager 认为热更已完成(本地有热更目录),不重新下载,直接用损坏文件启动,轻则白屏,重则 crash loop——用户无法自愈,只能卸载重装,差评和流失率直接拉高。
怎么做:
export class HotUpdateBootstrap {
private static readonly HOT_PATH = native.fileUtils.getWritablePath() + "hot/";
private static readonly BACKUP_PATH = native.fileUtils.getWritablePath() + "hot_backup/";
private static readonly MANIFEST_FILE = HotUpdateBootstrap.HOT_PATH + "project.manifest";
// 热更完成后调用,备份当前热更目录
static backupCurrentVersion() {
if (native.fileUtils.isDirectoryExist(this.HOT_PATH)) {
// 删除旧备份,将当前热更目录复制为备份
native.fileUtils.removeDirectory(this.BACKUP_PATH);
native.fileUtils.copyDirectory(this.HOT_PATH, this.BACKUP_PATH);
}
}
// 启动时自检,失败则回滚
static selfCheck(): boolean {
if (!native.fileUtils.isFileExist(this.MANIFEST_FILE)) return true; // 无热更目录,用包内资源
// 校验关键文件是否存在(可扩展为 MD5 校验)
const keyFiles = ["src/game.js", "assets/main/index.js"];
for (const f of keyFiles) {
if (!native.fileUtils.isFileExist(this.HOT_PATH + f)) {
console.warn("热更目录损坏,执行回滚:", f);
this.rollback();
return false;
}
}
return true;
}
static rollback() {
native.fileUtils.removeDirectory(this.HOT_PATH);
if (native.fileUtils.isDirectoryExist(this.BACKUP_PATH)) {
native.fileUtils.copyDirectory(this.BACKUP_PATH, this.HOT_PATH);
console.log("已回滚到上一版本");
} else {
console.log("无备份可用,使用包内版本");
}
}
}
规则: 热更完成(UPDATE_FINISHED)后必须调用 native.fileUtils.setSearchPaths,将热更存储目录置于搜索路径列表的第一位;设置后调用 native.AssetsManager.prepareFinish 更新引擎内部状态,最后重启游戏(cc.game.restart())让新搜索路径生效。
为什么: 这是热更新最容易被遗漏的最后一步,也是"热更成功但资源还是旧的"这一 bug 的根本原因。引擎加载资源时按搜索路径列表顺序查找,如果热更目录不在第一位,仍然优先加载包内旧资源,热更的新文件完全不生效。另一个常见错误:只在热更完成时设置搜索路径,但游戏下次冷启动时没有在初始化阶段恢复这个搜索路径,导致第一次冷启动后热更内容消失。搜索路径必须在每次应用启动时从本地存储读取并重新设置。
怎么做:
export class SearchPathManager {
private static readonly STORAGE_KEY = "hot_search_paths";
// 每次应用启动时调用,恢复上次设置的搜索路径
static restoreSearchPaths() {
if (!native.fileUtils) return;
const saved = sys.localStorage.getItem(this.STORAGE_KEY);
if (saved) {
const paths: string[] = JSON.parse(saved);
native.fileUtils.setSearchPaths(paths);
console.log("恢复热更搜索路径:", paths);
}
}
// 热更完成后调用
static applyHotUpdatePaths(hotStoragePath: string, am: native.AssetsManager) {
// ✅ 获取热更管理器建议的搜索路径(包含热更目录和包内目录)
const newPaths = am.getLocalManifest().getSearchPaths();
// ✅ 持久化搜索路径,下次冷启动时恢复
sys.localStorage.setItem(this.STORAGE_KEY, JSON.stringify(newPaths));
native.fileUtils.setSearchPaths(newPaths);
// ✅ 通知引擎热更完成,准备重启
native.AssetsManager.prepareFinish();
// ✅ 重启游戏使新路径生效
cc.game.restart();
}
}
// 在 main.ts 或首个场景的 onLoad 最顶部调用
SearchPathManager.restoreSearchPaths();
// 反例 — 只监听 UPDATE_FINISHED,忘设搜索路径,不重启
@ccclass
export class BadHotUpdate extends Component {
start() {
const am = new native.AssetsManager(
"project.manifest",
native.fileUtils.getWritablePath()
);
am.setEventCallback((event) => {
// ❌ 只处理成功,ERROR_DOWNLOAD / ERROR_VERIFY 全部忽略
if (event.getEventCode() === native.AssetsManager.EventCode.UPDATE_FINISHED) {
console.log("热更完成");
// ❌ 没有 setSearchPaths,热更文件根本不会被加载
// ❌ 没有 game.restart(),内存里还是旧资源
}
});
am.update();
}
}
// 正例 — 完整事件处理 + 搜索路径持久化 + 重启生效
@ccclass
export class GoodHotUpdate extends Component {
private _hum = new HotUpdateManager();
async start() {
if (!native.AssetsManager) return; // ✅ 非原生平台直接跳过
// ✅ 启动时先恢复上次的搜索路径
SearchPathManager.restoreSearchPaths();
const storagePath = native.fileUtils.getWritablePath() + "hot/";
this._hum.init("project.manifest", storagePath);
const hasUpdate = await this._hum.checkUpdate();
if (!hasUpdate) { this.startGame(); return; }
// ✅ 展示进度 UI
const hasError = await this._hum.update((p) => {
this._progressBar.progress = p;
}).then(() => false).catch((err) => {
console.error("热更失败:", err);
return true;
});
if (hasError) {
// ✅ 热更失败走回滚,不让用户卡死
HotUpdateBootstrap.rollback();
this.startGame(); // 用回滚版本继续
} else {
// ✅ 热更成功:持久化搜索路径 + 重启
const am = (this._hum as any)._am as native.AssetsManager;
SearchPathManager.applyHotUpdatePaths(storagePath, am);
// applyHotUpdatePaths 内部会调用 game.restart()
}
}
private startGame() {
director.loadScene("Hall");
}
}
// 反例 — manifest 版本号写死,不自动生成,导致热更不触发
// project.manifest (错误示例,版本号从未变过)
// {
// "version": "1.0", ❌ 每次热更都是 1.0,客户端认为无需更新
// "assets": {
// "src/game.js": { "md5": "abc123" } ❌ 手写 md5,与实际文件不符
// }
// }
# 正例 — CI/CD 构建脚本自动生成 manifest,版本号用构建时间戳
# (在 Jenkins/GitHub Actions 的构建步骤中)
node version-generator.js \
--src ./build/jsb-link/res \
--dest ./cdn/updates/ \
--url https://cdn.example.com/updates/ \
--version $(date +%Y%m%d%H%M)
# ✅ 每次构建自动扫描文件 MD5,版本号用时间戳保证递增
# ✅ 生成的 project.manifest 和 version.manifest 立即上传 CDN
project.manifest 由构建脚本自动生成,版本号每次递增,没有手写 MD5。version.manifest 和 project.manifest 设置了极短 Cache-Control(≤60s 或 no-store)。ERROR_DOWNLOAD、ERROR_VERIFY、ERROR_FAILED_DECOMPRESS、UPDATE_FAILED 全部错误事件,每个都有对应处理逻辑。setSearchPaths,并将路径持久化到 localStorage,每次冷启动时恢复。native.AssetsManager.prepareFinish() 和 cc.game.restart(),新资源才真正生效。