CocosCreator 3.x热更新学习

最近学习了一下CocosCreator的热更新相关知识,记录下学习过程。

Creator版本:v3.5.2

官方范例学习文档:热更新范例教程 · Cocos Creator

官方范例Demo链接:mirrors_cocos-creator/tutorial-hot-update - Gitee.com

官方热更新原理详细介绍:热更新管理器 · Cocos Creator

建议先把官方文档认真仔细看一遍。

重要工具:

1:官方Demo中的cocos-tutorial-hot-update-master\extensions\hot-update编辑器插件。

2:官方Demo中的version_generator.js脚本。

一、创建HotUpdateDemo工程

把官方范例里面的extensions\hot-update文件夹和version_generator.js脚本复制到工程中,如下图所示:

 二、搭建一个简易的热更新界面

 

 三、编辑HotUpdateController.ts脚本

import { _decorator, Component, Node, Label, Asset, resources, game, ProgressBar, Button } from 'cc';
import { JSB, NATIVE } from 'cc/env';
const { ccclass, property } = _decorator;

@ccclass('HotUpdateController')
export class HotUpdateController extends Component {
    @property(Label)
    lbCheckUpdate:Label = null;
    
    @property(Node)
    nodeUpdateDialog:Node = null;

    @property(Label)
    lbFilesCount:Label = null;

    @property(Label)
    lbFilesSize:Label = null;

    @property(Asset)
    manifestUrl: Asset = null;

    @property(ProgressBar)
    pbFilesCount:ProgressBar = null;

    @property(ProgressBar)
    pbFilesSize:ProgressBar = null;

    @property(Button)
    btnUpdate:Button = null;


    private hotUpdateAM:jsb.AssetsManager = null;

    start() {
        this.nodeUpdateDialog.active = false;
        if(!NATIVE || !JSB){
            this.node.active = false;
            return;
        }

        let storagePath = jsb.fileUtils.getWritablePath() + "blackjack-remote-asset";
        this.hotUpdateAM = new jsb.AssetsManager("", storagePath, (versionA: string, versionB: string)=>{
            console.log("HotUpdate JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB);
            var vA = versionA.split('.');
            var vB = versionB.split('.');
            for (var i = 0; i < vA.length; ++i) {
                var a = parseInt(vA[i]);
                var b = parseInt(vB[i] || '0');
                if (a === b) {
                    continue;
                }
                else {
                    return a - b;
                }
            }
            if (vB.length > vA.length) {
                return -1;
            }
            else {
                return 0;
            }
        });


        this.hotUpdateAM.setVerifyCallback(function (path: string, asset: any) {
            // When asset is compressed, we don't need to check its md5, because zip file have been deleted.
            var compressed = asset.compressed;
            // Retrieve the correct md5 value.
            var expectedMD5 = asset.md5;
            // asset.path is relative path and path is absolute.
            var relativePath = asset.path;
            // The size of asset file, but this value could be absent.
            var size = asset.size;
            if (compressed) {
                console.log("HotUpdate", "Verification passed : " + relativePath);
                return true;
            }
            else {
                console.log("HotUpdate", "Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
                return true;
            }
        });

        this.checkUpdate();

        this.btnUpdate.node.on(Node.EventType.TOUCH_END, ()=>{this.dealUpdate();}, this)
    }

    private checkUpdate(){
        if (this.hotUpdateAM.getState() === jsb.AssetsManager.State.UNINITED) {
            console.log("HotUpdate", "use local manifest");
            var url = this.manifestUrl.nativeUrl;
            this.hotUpdateAM.loadLocalManifest(url);
        }
        if (!this.hotUpdateAM.getLocalManifest() || !this.hotUpdateAM.getLocalManifest().isLoaded()) {
            this.lbCheckUpdate.string = 'Failed to load local manifest ...';
            return;
        }
        this.hotUpdateAM.setEventCallback((event: jsb.EventAssetsManager)=>{
            switch(event.getEventCode()){
                case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                    this.lbCheckUpdate.string = "No local manifest file found, hot update skipped.";
                    this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
                    break;
                case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                    this.lbCheckUpdate.string = "Fail to download manifest file, hot update skipped.";
                    this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
                    break;
                case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                    this.lbCheckUpdate.string = "Already up to date with the latest remote version.";
                    this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
                    break;
                case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                    this.lbCheckUpdate.string = 'New version found, please try to update. (' + Math.ceil(this.hotUpdateAM.getTotalBytes() / 1024) + 'kb)';
                    this.lbFilesCount.string = "文件数量:" + this.hotUpdateAM.getTotalFiles().toString();
                    this.lbFilesSize.string = "文件大小:" + Math.ceil(this.hotUpdateAM.getTotalBytes() / 1024).toString() + "kb";
                    this.nodeUpdateDialog.active = true;
                    break;
                default:
                    return;
            }
        });

        this.hotUpdateAM.checkUpdate();
    }

    private dealUpdate(){
        if (this.hotUpdateAM.getState() === jsb.AssetsManager.State.UNINITED) {
            console.log("HotUpdate", "use local manifest");
            var url = this.manifestUrl.nativeUrl;
            this.hotUpdateAM.loadLocalManifest(url);
        }
        if (!this.hotUpdateAM.getLocalManifest() || !this.hotUpdateAM.getLocalManifest().isLoaded()) {
            this.lbCheckUpdate.string = 'Failed to load local manifest ...';
            return;
        }

        this.btnUpdate.node.active = false;
        this.hotUpdateAM.setEventCallback((event:jsb.EventAssetsManager)=>{
            let needRestart = false;
            switch (event.getEventCode()) {
                case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
                    this.lbCheckUpdate.string = 'No local manifest file found, hot update skipped.';
                    break;
                case jsb.EventAssetsManager.UPDATE_PROGRESSION:
                    this.pbFilesSize.progress = event.getPercent();
                    this.pbFilesCount.progress = event.getPercentByFile();
                    break;
                case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
                    this.lbCheckUpdate.string = 'Fail to download manifest file, hot update skipped.';
                    break;
                case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
                    this.lbCheckUpdate.string = 'Already up to date with the latest remote version.';
                    break;
                case jsb.EventAssetsManager.UPDATE_FINISHED:
                    this.lbCheckUpdate.string = 'Update finished. ' + event.getMessage();
                    this.pbFilesSize.progress = 1;
                    this.pbFilesCount.progress = 1;
                    needRestart = true;
                    break;
                case jsb.EventAssetsManager.UPDATE_FAILED:
                    this.lbCheckUpdate.string = 'Update failed. ' + event.getMessage();
                    break;
                case jsb.EventAssetsManager.ERROR_UPDATING:
                    this.lbCheckUpdate.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage();
                    break;
                case jsb.EventAssetsManager.ERROR_DECOMPRESS:
                    this.lbCheckUpdate.string = event.getMessage();
                    break;
                default:
                    break;
            }

            if (needRestart) {
                this.hotUpdateAM.setEventCallback(null!);
                // Prepend the manifest's search path
                var searchPaths = jsb.fileUtils.getSearchPaths();
                var newPaths = this.hotUpdateAM.getLocalManifest().getSearchPaths();
                console.log("HotUpdate", JSON.stringify(newPaths));
                Array.prototype.unshift.apply(searchPaths, newPaths);
                // This value will be retrieved and appended to the default search path during game startup,
                // please refer to samples/js-tests/main.js for detailed usage.
                // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
                localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
                jsb.fileUtils.setSearchPaths(searchPaths);
                console.log("HotUpdate SearchPaths:", searchPaths);
    
                // restart game.
                setTimeout(() => {game.restart();}, 1000);
            }
        });

        this.hotUpdateAM.update();
    }
}

四、第一次构建(Android)

构建完成之后,会出现build目录。

使用node命令,运行version_generator.js脚本,生成project.manifest和version.manifest文件

 根据node命令制作version_generator.bat批处理文件,方便后续.manifest文件生成

双击version_generator.bat,会生成remote-assets目录,目录中会生成project.manifest和version.manifest文件

把 project.manifest和version.manifest文件复制到assets目录下,并挂载到脚本上HotUpdateController.ts脚本上 

五、第二次构建并打出v1.0.0版本的apk包,使用模拟器打开,会出现如下三个界面切换

本步骤完成之后,就算完成了第一个包了,后续我们就需要构建更新包放到远程服务器,然后通过网络热更新来给安装到模拟器上的这个包进行更新。

六、修改游戏内容,第三次构建出v1.0.1版本的资源。

将Label控件的string由Hello改成Hello,Cocos,然后重新构建出新的资源

注意:只需要构建,不需要生成apk包。

修改version_generator.bat文件中的版本号为1.0.1,生成1.0.1版本的project.manifest和version.manifest文件。

七、本地搭建一个临时服务器。

在电脑任意位置创建一个HotUpdate/remote-assets目录,并且把HotUpdateDemo\build\android\assets目录下的新构建的资源和HotUpdateDemo\remote-assets下的.manifest文件复制进去,如下图所示:

 利用live-server搭建简易服务器,成功之后会在浏览器中能访问到HotUpdate/remote-assets目录中的所有文件。

 八、重启模拟器中的安装的app

 至此,一个粗略的热更新流程就完成了。

问题一:只有热更新本次生效,重启app之后就不生效了

检查HotUpdateDemo\build\android\assets\mian.js中是否有如下代码:

这段代码是 HotUpdateDemo\extensions\hot-update插件在构建完成之后,自动加到main.js中的,构建完成时,Creator编辑器控制台也会有如下日志:

 如果没有,需要检查这个热更新插件为何没有生效。

问题二:搜索路径会随着热更新次数的增加而增多。

if (needRestart) {
    this.hotUpdateAM.setEventCallback(null!);
    // Prepend the manifest's search path
    var searchPaths = jsb.fileUtils.getSearchPaths();
    var newPaths = this.hotUpdateAM.getLocalManifest().getSearchPaths();
    console.log("HotUpdate", JSON.stringify(newPaths));
    Array.prototype.unshift.apply(searchPaths, newPaths);
    // This value will be retrieved and appended to the default search path during game startup,
    // please refer to samples/js-tests/main.js for detailed usage.
    // !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
    localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
    jsb.fileUtils.setSearchPaths(searchPaths);
    console.log("HotUpdate SearchPaths:", searchPaths);

    // restart game.
    setTimeout(() => {game.restart();}, 1000);
}

这段代码是官方Demo里面的,主要作用就是把热更新的新资源路径放在2dx引擎搜索路径的最前面,这样新下载的资源就会优先被搜索到从而达到热更新的目的。

但是这段代码会有一个问题,如果searchPaths数组中本来就存在newPaths,这个时候再往数组前面加一个newPaths,会导致搜索路径的前N个是相同的。

通过这段日志输出也可以看出来,当从1.0.0->1.0.1->1.0.2之后,搜索路径里面已经出现了3条一模一样的搜索路径,所以这里在unshift.apply之前需要判断下原数组中是否存在新路径。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值