I’ve implemented the Cocos Creator Hot Update system for my Android project. The update process seems to work fine, the files are successfully downloaded from the remote server, and I reset/restart the game after the update.
However, after restarting, the game still runs with the old assets and logic. The newly downloaded files don’t seem to take effect.
import { _decorator, assetManager, Component, instantiate, JsonAsset, Node, Prefab, sys, SpriteFrame, Texture2D, AudioClip, Sprite, AudioSource, native, Asset, game, director, AssetManager, Label, ProgressBar, Button } from ‘cc’;
import { NATIVE } from ‘cc/env’;
const { ccclass, property } = _decorator;
@ccclass(‘RemoteEntryLoader’)
export class RemoteEntryLoader extends Component {
@property(Asset)
manifestUrl: Asset = null!;
@property(Label)
statusLabel: Label = null!;
@property(ProgressBar)
progressBar: ProgressBar = null!;
@property(Button)
checkUpdateBtn: Button = null!;
@property(Button)
retryBtn: Button = null!;
private assetsManager: any = null;
private storagePath: string = ‘’;
// private manifestUrl: string = ‘’;
private updating: boolean = false;
private canRetry: boolean = false;
private updateListener: any = null;
onLoad() {
// Only initialize on native platforms
if (!NATIVE) {
this.statusLabel.string = ‘Hot update is only available on native platforms’;
return;
}
// Setup paths - customize these for your project
this.storagePath = ((native.fileUtils && native.fileUtils.getWritablePath) ?
native.fileUtils.getWritablePath() : '/') + 'hot-update/';
// Your remote manifest URL - replace with your server URL
// this.manifestUrl = 'https://thanisthani.github.io/CocosHotUpdate/remote-assets/project.manifest';
this.initHotUpdate();
this.setupUI();
}
private initHotUpdate(): void {
if (!NATIVE) return;
try {
if (NATIVE) {
const hotUpdateRoot = native.fileUtils.getWritablePath() + 'hot-update/';
console.log('Hot update directory exists:', native.fileUtils.isDirectoryExist(hotUpdateRoot));
console.log('Manifest exists:', native.fileUtils.isFileExist(hotUpdateRoot + 'project.manifest'));
// List files in hot update directory
const files = native.fileUtils.listFiles(hotUpdateRoot);
console.log('Files in hot update directory:', files);
}
// Ensure storage directory exists
if (!native.fileUtils.isDirectoryExist(this.storagePath)) {
native.fileUtils.createDirectory(this.storagePath);
}
// Create AssetsManager instance
this.assetsManager = new native.AssetsManager(this.manifestUrl.nativeUrl, this.storagePath);
// Set up search paths for hot updated assets
this.setupSearchPaths();
// Configure AssetsManager
this.assetsManager.setMaxConcurrentTask(5); // Limit concurrent downloads
// Set version comparison function (optional)
this.assetsManager.setVersionCompareHandle((versionA: string, versionB: string) => {
return this.compareVersion(versionA, versionB);
});
// Set file verification callback (optional but recommended)
this.assetsManager.setVerifyCallback((path: string, asset: any) => {
return this.verifyAsset(path, asset);
});
// Set update event listener
this.updateListener = (event: any) => this.updateCallback(event);
this.assetsManager.setEventCallback(this.updateListener);
this.statusLabel.string = 'Hot update initialized';
} catch (error) {
console.error('Failed to initialize hot update:', error);
this.statusLabel.string = 'Failed to initialize hot update';
}
}
private setupSearchPaths(): void {
if (!NATIVE || !this.assetsManager) return;
try {
if (this.assetsManager.getState() === native.AssetsManager.State.UNINITED) {
console.log("loadLocalManifest is called")
if (this.manifestUrl && this.manifestUrl.nativeUrl) {
this.assetsManager.loadLocalManifest(this.manifestUrl.nativeUrl);
}
}
// Get hot update search paths from local manifest
const localManifest = this.assetsManager.getLocalManifest();
if (!localManifest || !localManifest.isLoaded()) {
console.log("Failed to load bundled manifest");
throw new Error('Failed to load bundled manifest');
}
if (localManifest && localManifest.isLoaded()) {
console.log("localManifest is loaded");
const hotUpdateSearchPaths = localManifest.getSearchPaths();
const searchPaths = native.fileUtils.getSearchPaths();
// Insert hot update paths at the beginning for priority
Array.prototype.unshift.apply(searchPaths, hotUpdateSearchPaths);
native.fileUtils.setSearchPaths(searchPaths);
console.log('Search paths updated:', searchPaths);
}
} catch (error) {
console.error('Failed to setup search paths:', error);
}
}
private setupUI(): void {
// Setup button callbacks
if (this.checkUpdateBtn) {
this.checkUpdateBtn.node.on(‘click’, this.checkForUpdate, this);
}
if (this.retryBtn) {
this.retryBtn.node.on('click', this.retry, this);
this.retryBtn.node.active = false;
}
}
private checkForUpdate(): void {
if (!NATIVE || !this.assetsManager || this.updating) return;
console.log("checkForUpdate is called");
this.updating = true;
this.canRetry = false;
this.checkUpdateBtn.node.active = false;
this.retryBtn.node.active = false;
this.statusLabel.string = 'Checking for updates...';
this.progressBar.progress = 0;
// Check for updates
this.assetsManager.checkUpdate();
}
private retry(): void {
if (!this.canRetry) return;
this.canRetry = false;
this.retryBtn.node.active = false;
this.statusLabel.string = 'Retrying failed downloads...';
// Retry downloading failed assets
this.assetsManager.downloadFailedAssets();
}
private updateCallback(event: any): void {
const eventCode = event.getEventCode();
console.log(“update eventCode is”, eventCode);
switch (eventCode) {
case native.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.statusLabel.string = 'No local manifest file found';
this.onUpdateFinished(false);
break;
case native.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
this.statusLabel.string = 'Failed to download manifest';
this.onUpdateFinished(false);
break;
case native.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.statusLabel.string = 'Failed to parse manifest';
this.onUpdateFinished(false);
break;
case native.EventAssetsManager.NEW_VERSION_FOUND:
this.statusLabel.string = 'New version found, starting download...';
this.assetsManager.update();
break;
case native.EventAssetsManager.ALREADY_UP_TO_DATE:
this.statusLabel.string = 'Already up to date';
this.onUpdateFinished(true);
break;
case native.EventAssetsManager.UPDATE_PROGRESSION:
this.handleUpdateProgress(event);
break;
case native.EventAssetsManager.ASSET_UPDATED:
// Individual asset updated
break;
case native.EventAssetsManager.ERROR_UPDATING:
this.statusLabel.string = 'Update error: ' + event.getMessage();
this.onUpdateFinished(false);
break;
case native.EventAssetsManager.UPDATE_FINISHED:
this.statusLabel.string = 'Update completed! Restart required.';
const searchPaths = native.fileUtils.getSearchPaths();
console.log("searchPaths is", searchPaths);
const hotUpdatePath = this.storagePath;
// Remove if exists, then add at beginning
const index = searchPaths.indexOf(hotUpdatePath);
if (index > -1) searchPaths.splice(index, 1);
searchPaths.unshift(hotUpdatePath);
native.fileUtils.setSearchPaths(searchPaths);
console.log("updated searchPaths is", searchPaths);
// Save restart flag
sys.localStorage.setItem('hotUpdateReady', 'true');
sys.localStorage.setItem('hotUpdatePath', hotUpdatePath);
this.onUpdateFinished(true, true);
break;
case native.EventAssetsManager.UPDATE_FAILED:
this.statusLabel.string = 'Update failed: ' + event.getMessage();
this.canRetry = true;
this.retryBtn.node.active = true;
this.onUpdateFinished(false);
break;
case native.EventAssetsManager.ERROR_DECOMPRESS:
this.statusLabel.string = 'Decompression failed';
this.onUpdateFinished(false);
break;
}
}
private handleUpdateProgress(event: any): void {
const percent = event.getPercent();
const filePercent = event.getPercentByFile();
const downloadedBytes = event.getDownloadedBytes();
const totalBytes = event.getTotalBytes();
const downloadedFiles = event.getDownloadedFiles();
const totalFiles = event.getTotalFiles();
// Update progress bar (using byte progress)
this.progressBar.progress = percent / 100;
// Update status label with detailed progress
this.statusLabel.string = `Downloading... ${Math.floor(percent)}%\n` +
`Files: ${downloadedFiles}/${totalFiles}\n` +
`Size: ${this.formatBytes(downloadedBytes)}/${this.formatBytes(totalBytes)}`;
console.log(`Downloading... ${Math.floor(percent)}%\n` +
`Files: ${downloadedFiles}/${totalFiles}\n` +
`Size: ${this.formatBytes(downloadedBytes)}/${this.formatBytes(totalBytes)}`)
}
private onUpdateFinished(success: boolean, needRestart: boolean = false): void {
this.updating = false;
this.checkUpdateBtn.node.active = true;
if (needRestart) {
// Show restart button or automatically restart
this.showRestartOption();
}
}
private showRestartOption(): void {
// You can show a restart dialog here
// For now, we’ll just add a restart button functionality
this.statusLabel.string = ‘Update completed! Tap to restart.’;
this.checkUpdateBtn.getComponentInChildren(Label)!.string = ‘Restart Game’;
this.checkUpdateBtn.node.off(‘click’, this.checkForUpdate, this);
this.checkUpdateBtn.node.on(‘click’, this.restartGame, this);
}
private restartGame(): void {
console.log(“restartGame is called”);
// Clean up before restart
if (this.assetsManager) {
this.assetsManager.setEventCallback(null);
}
// Restart the game
game.restart();
}
private compareVersion(versionA: string, versionB: string): number {
// Custom version comparison logic
// Return > 0 if versionA > versionB
// Return 0 if versionA == versionB
// Return < 0 if versionA < versionB
const parseVersion = (version: string) => {
return version.split('.').map(v => parseInt(v) || 0);
};
const vA = parseVersion(versionA);
const vB = parseVersion(versionB);
for (let i = 0; i < Math.max(vA.length, vB.length); i++) {
const a = vA[i] || 0;
const b = vB[i] || 0;
if (a !== b) return a - b;
}
return 0;
}
private verifyAsset(filePath: string, asset: any): boolean {
// Asset verification logic
// You can implement MD5 check here if needed
// For now, just return true to skip verification
return true;
}
private formatBytes(bytes: number): string {
if (bytes === 0) return ‘0 B’;
const k = 1024;
const sizes = [‘B’, ‘KB’, ‘MB’, ‘GB’];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ’ ’ + sizes[i];
}
onDestroy(): void {
// Clean up
if (this.assetsManager && this.updateListener) {
this.assetsManager.setEventCallback(null);
}
}
}