Cocos3.15 js 热更新实践

没想到我上篇博文还在写作中,cocos就出了新版3.15,好像对音频播放做了优化,挺好的,但是热更新也修改了点,好吧,其实我之前修改了它的热更新机制,实现序列化更新,就是1.0.0 更新到1.0.1然后再更新到1.0.2。好吧,这个方法的缺陷就是不能删除以前不要的文件。但是cocos自己的新的更新机制也没有去删除不要的文件啊,-_-,看下面的代码:

if (diff.type != Manifest::DiffType::DELETED)
{
        std::string path = diff.asset.path;
        DownloadUnit unit;
        unit.customId = it->first;
        unit.srcUrl = packageUrl + path;
        unit.storagePath = _tempStoragePath + path;
        unit.size = diff.asset.size;
        _downloadUnits.emplace(unit.customId, unit);
        _tempManifest->setAssetDownloadState(it->first,Manifest::DownloadState::UNSTARTED);
 }

唯一用到Manifest::DiffType::DELETED的地方。就是如果我们的asset资源如果不是删除状态,那么我们记录需要下载。

3.15下载文件如果需要解压缩,那么直接解压缩,3.14.1还是添加到一个待解压缩列表,于是我在3.14.1中做了个修改,让他们安装顺序填入map中,然后依次解压缩,后面解压的覆盖前面,这样我们就实现了跨版本序列化更新。可是3.15的改动导致我得重新修改很多代码。

cocos的本意是所有的src和res的每个文件都是单独的assets资源,这样可以改动,或添加的文件就会下载更新,但是这样的话,打包更新包麻烦,每个文件都需要单独打包,就算写脚本处理也觉得不大靠谱,更何况之前遇到的游戏更新,大家都是一个版本一个版本的更新上去的,并不是直接跳到某个版本,亦或是我觉得这样更新不爽吧,我觉得还是一个版本一个版本更新舒服一点。哈哈。。。

好,那我们先开始修改源码。

找到 AssetsManagerEx.cpp:

改动1

添加代码:

#define MAX_DOWNLOAD_TASK 1
改动2
network::DownloaderHints hints =
    {
        static_cast(_maxConcurrentTaskK),
        DEFAULT_CONNECTION_TIMEOUT,
        ".tmp"
    };

改成

network::DownloaderHints hints =
    {
        static_cast(/*_maxConcurrentTask*/MAX_DOWNLOAD_TASK),
        DEFAULT_CONNECTION_TIMEOUT,
        ".tmp"
    };
改动3
while (_currConcurrentTask < _maxConcurrentTask && _queue.size() > 0)

改成

while (_currConcurrentTask < /*_maxConcurrentTask*/MAX_DOWNLOAD_TASK && _queue.size() > 0)

意思很简单,我们下载任务只执行一个!!!

c++代码修改完成。
那我们先下载Tomcat,安装完成后,打开开始菜单的

Cocos3.15 js 热更新实践_第1张图片
开始菜单

如果你能看到下面的图片:

Cocos3.15 js 热更新实践_第2张图片
Apache欢迎页

说明你已经安装成功了。好先放一边。。。

创建一个cocos js工程,

  1. 下载安装cocos3.15。
  2. 下载安装python2.7。
  3. 打开cocos3.15环境,我的是:


    Cocos3.15 js 热更新实践_第3张图片
    cocos3.15安装环境
  4. 执行setup.py,根据提示,部署环境。
  5. 运行cmd,执行脚本cocos new -p com.hangzhou.test -d D:\xxx -l js ,最好事先创建目录xxx目录,xxx是你希望项目存放的目录
  6. 下载安装webstorm
  7. 用webstorm打开我们的xxx目录。

修改我们的代码

在src目录新建两个文件:updateList.js和assetsManagerScene.js文件

文件内容是:

updateList.js 用于保存我们的更新js列表
var jsUpdateList = [
    "src/app.js",
    "src/resource.js"
];
assetsManagerScene.js 热更新场景
/**
 * 热更新
 */

var failCount = 0;

var maxFailCount = 0;   //最大错误重试次数

/**

 * 自动更新js和资源

 */

var AssetsManagerLoaderScene = cc.Scene.extend({

    _am: null,

    _progress: null,

    _percent: 0,

    run: function () {

        if (!cc.sys.isNative) {

            this.loadGame();

            return;

        }

        var layer = new cc.Layer();

        this.addChild(layer);

        this._progress = new cc.LabelTTF.create("update____0%", "Arial", 12);

        this._progress.x = cc.winSize.width / 2;

        this._progress.y = cc.winSize.height / 2 + 50;

        layer.addChild(this._progress);

        var storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "./");

        cc.log("storagePath is " + storagePath);

        this._am = new jsb.AssetsManager("res/Manifests/project.manifest", storagePath);

        this._am.retain();

        if (!this._am.getLocalManifest().isLoaded()) {

            cc.log("Fail to update assets, step skipped.");
            this.loadGame();

        } else {

            var that = this;

            //cc.EventListenerAssetsManager

            var listener = new jsb.EventListenerAssetsManager(this._am, function (event) {

                switch (event.getEventCode()) {

                    case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:

                        cc.log("assetsManagerScene : ERROR_NO_LOCAL_MANIFEST  " + event.getMessage());
                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
                        cc.log("assetsManagerScene : ERROR_DOWNLOAD_MANIFEST  " + event.getMessage());

                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:

                        cc.log("assetsManagerScene : ERROR_PARSE_MANIFEST  " + event.getMessage());

                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.NEW_VERSION_FOUND:
                        cc.log("assetsManagerScene : NEW_VERSION_FOUND " + event.getMessage());

                        //我们需要更新。。。
                        break;
                    case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:

                        cc.log("assetsManagerScene : ALREADY_UP_TO_DATE " + event.getMessage());
                        //我们不需要更新。。。
                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.UPDATE_PROGRESSION://更新进度条

                        cc.log("assetsManagerScene : UPDATE_PROGRESSION " + event.getPercent() + "," + event.getMessage());
                        that._percent = event.getPercent();
                        var msg = event.getMessage();
                        if (msg) {
                            cc.log(msg);
                        }
                        that.updateProgress(event.getPercent());

                        break;
                    case jsb.EventAssetsManager.ASSET_UPDATED://资源更新完毕,可能还需要解压缩
                        cc.log("assetsManagerScene : ASSET_UPDATED " + event.getAssetId() + "," + event.getMessage());
                        break;
                    case jsb.EventAssetsManager.ERROR_UPDATING://更新发生错误

                        cc.log("assetsManagerScene : ERROR_UPDATING " + event.getAssetId() + ", " + event.getMessage());

                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.UPDATE_FINISHED://资源更新完毕

                        cc.log("assetsManagerScene : UPDATE_FINISHED " + event.getMessage());

                        that.loadGame();

                        break;
                    case jsb.EventAssetsManager.UPDATE_FAILED://更新失败

                        cc.log("assetsManagerScene : UPDATE_FAILED " + event.getMessage());

                        failCount++;

                        if (failCount < maxFailCount) {

                            that._am.downloadFailedAssets();
                        }

                        else {

                            cc.log("Reach maximum fail count, exit update process");

                            failCount = 0;

                            that.loadGame();

                        }

                        break;
                    case jsb.EventAssetsManager.ERROR_DECOMPRESS://解压错误

                        cc.log("assetsManagerScene : ERROR_DECOMPRESS " + event.getMessage());

                        that.loadGame();

                        break;

                    default:
                        cc.log("assetsManagerScene : unknown Event" + event.getMessage());
                        that.loadGame();
                        break;

                }

            });

            cc.eventManager.addListener(listener, 1);

            this._am.update();

            cc.director.runScene(this);

        }
    },

    loadGame: function () {
        //jsList是jsList.js的变量,记录全部js。
        if (cc.sys.isNative) {

            cc.loader.loadJs("", ["src/updateList.js"], function (error) {
                if (error)
                    console.log("load js src/updateList.js error=" + error);
                else
                    console.log("load js src/updateList.js success");

                // cc.loader.loadImg() // 图片放后边再说
                cc.loader.loadJs("", jsUpdateList, function (error) {
                    if (error)
                        console.log("load js error=" + error);
                    else
                        console.log("load js success");

                    cc.director.runScene(new HelloWorldScene());
                });

            });
        }
    },

    updateProgress: function (percent) {
        this._progress.string = "update__" + percent + "__%";
    },

    onExit: function () {

        cc.log("AssetsManager::onExit");

        this._am.release();

        this._super();

    }

});

修改我们的main.js文件:

    cc.LoaderScene.preload(g_resources, function () {
        cc.director.runScene(new HelloWorldScene());

    }, this);

改成:

    // cc.LoaderScene.preload(g_resources, function () {
    //     cc.director.runScene(new HelloWorldScene());
    //
    // }, this);

    var scene = new AssetsManagerLoaderScene();

    scene.run();

修改project.json,改成这样

"jsList" : [
        "src/assetsManagerScene.js"
    ]

在res/Manifests目录下添加一个版本文件:project.manifest

{
    "packageUrl": "http://192.168.1.148:8080/cocos/test/",
    "remoteManifestUrl": "http://192.168.1.148:8080/cocos/test/project.manifest",
    "remoteVersionUrl": "http://192.168.1.148:8080/cocos/test/version.manifest",
    "engineVersion": "3.15",
    "version": "1.0.0",

    "assets": {
    },
    "searchPaths": [
        "update"
    ]
}

最终webstorm的目录结构是:

Cocos3.15 js 热更新实践_第4张图片
目录结构

打开vs项目


Cocos3.15 js 热更新实践_第5张图片
vs解决方案

运行项目,我们可以看到我们的更新场景:

Cocos3.15 js 热更新实践_第6张图片
更新场景

大约4秒后会呈现我们的游戏场景

Cocos3.15 js 热更新实践_第7张图片
游戏场景

由于我们还没配置我们的热更新服务器,所以有4秒的超时等待时间。

下面配置我们的热更新服务器:
打开Apache安装目录,我的是在C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps,新建cocos目录,再在里面创建test目录。
得到这样一个目录:C:\Program Files\Apache Software Foundation\Tomcat 9.0\webapps\cocos\test
创建目录update,和project.manifest文件和version.manifest文件
结果图:

Cocos3.15 js 热更新实践_第8张图片
test目录

project.manifest就是我上面讲的文件,内容一样的。
version.manifest内容如下:

{
    "forceUpdate": true
    
    "packageUrl": "http://192.168.1.148:8080/cocos/test/",
    "remoteManifestUrl": "http://192.168.1.148:8080/cocos/test/project.manifest",
    "remoteVersionUrl": "http://192.168.1.148:8080/cocos/test/version.manifest",
    "engineVersion": "3.15",
    "version": "1.0.0"
}

再运行我们的程序,发现我们很快就从更新场景到游戏场景了!!!

下面进行我们的服务器配置热更新

拷贝一份app.js到test,修改一行代码:

var helloLabel = new cc.LabelTTF("Hello World", "Arial", 38);

改成

var helloLabel = new cc.LabelTTF("Hello World 1", "Arial", 38);

新建src目录拷贝app.js到里面,然后添加到压缩包。起名app_js_1.0.1.zip:

Cocos3.15 js 热更新实践_第9张图片
app_js_1.0.1.zip

修改我们在test目录的project.manifest和version.manifest:

Cocos3.15 js 热更新实践_第10张图片
project.manifest
Cocos3.15 js 热更新实践_第11张图片
version.manifest

再次运行我们的程序,OK,见到我们的Hello World 1了!!!

Cocos3.15 js 热更新实践_第12张图片
Hello World 1

下面,我们重复操作,把上面那段显示文字改成Hello World 2,Hello World 3,每个都创建一个压缩包:

Cocos3.15 js 热更新实践_第13张图片
资源更新包

压缩包都一样都是包含一个src文件夹,里面存一个app.js文件。
继续修改我们的project.manifest和version.manifest文件:

Cocos3.15 js 热更新实践_第14张图片
project.manifest
Cocos3.15 js 热更新实践_第15张图片
version.manifest

重新运行我们的程序!如愿以偿看到我们的Hello World 3

Cocos3.15 js 热更新实践_第16张图片
Hello World 3

最后要说的话

这里cocos有一个坑,

"assets": {
        "update3": {
            "path": "update/app.js_1.0.3.zip",
            "md5": "9521D6B2176D667E22DC4F8345AE96E5",
            "compressed": true
        },
        "update2": {
            "path": "update/app.js_1.0.2.zip",
            "md5": "940002A15EA29516E3AC45AE073BE66C",
            "compressed": true
        },
        "update1": {
            "path": "update/app.js_1.0.1.zip",
            "md5": "88AA0BC4F6D9FE04A8532859674DE321",
            "compressed": true
        }
        
    }

我们所有的assets更新文件必须是倒序的,因为cocos执行下载的时候是倒着取的,

while (_currConcurrentTask < /*_maxConcurrentTask*/MAX_DOWNLOAD_TASK && _queue.size() > 0)
    {
        std::string key = _queue.back();//这里从back取下载的key,也就是我们的update1,update2,update3
        _queue.pop_back();
        
        _currConcurrentTask++;
        DownloadUnit& unit = _downloadUnits[key];
        _fileUtils->createDirectory(basename(unit.storagePath));
        _downloader->createDownloadFileTask(unit.srcUrl, unit.storagePath, unit.customId);
        
        _tempManifest->setAssetDownloadState(key, Manifest::DownloadState::DOWNLOADING);
    }

限制单个下载也是这个原因,我们如果多个任务同时下载,可能会导致覆盖文件出现问题,可能导致我们的Hello World 3不能显示,可能最后只显示Hello World 1

后续问题

后面发现文件多了,更新的下来的zip包也是多线程解压缩的,,然后可能会存在旧的压缩包解压出来的东西替换最新的压缩包解压出来的,,尴尬。。。可能还是只能支持整包更新吧。。。看了官方github上提供的manifest生成工具,它的思路就是把你所有的文件都记录在案,没有的,自己去下载。可能这样是目前最好的一种方法了。附上官方的manifest生成工具,使用之前需要安装nodejs

你可能感兴趣的:(Cocos3.15 js 热更新实践)