最近主要进行游戏脚本化相关工作,脚本化的目的就是为了热更新,所以就写个demo研究下热更新。
cocos版本: 3.12
此为官方说明的特性~~~~
- 多线程并行下载支持
- 两层进度统计信息:文件级以及字节级
- Zip压缩文件支持
- 断点续传
- 详细的错误报告
- 文件下载失败重试支持
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest",
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"engineVersion" : "3.12",
"groupVersions" : {
"1" : "1.0.0"
},
"assets" : {
},
"searchPaths" : [
]
}
其中地址为我自己的测试地址,各位看官自己进行相应的替换(这里有个坑,详见文末)
src目录下新建jsList.js文件,需要动态加载的js文件都写在jsFiles这个数组里,这样js文件有增加变化,这个files.js一并更新,方便动态加载,内容如下:
var jsList = [
"src/resource.js",
"src/app.js"
];
src目录下新建assetsManagerScene.js文件,内容如下:
/**
* Created by MartinYing on 2016/12/28.
*/
var failCount = 0;
var maxFailCount = 1; //最大错误重试次数
/**
* 自动更新js和资源
*/
var AssetsManagerLoaderScene = cc.Scene.extend({
_am:null,
_progress:null,
_percent:0,
run:function(){
cc.log("enter 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", 38);
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/project.manifest", storagePath);
this._am.retain();
if (!this._am.getLocalManifest().isLoaded())
//if (true)
{
cc.log("Fail to update assets, step skipped.");
this.loadGame();
}
else
{
var that = this;
var listener = new jsb.EventListenerAssetsManager(this._am, function(event) {
switch (event.getEventCode()){
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log("enter ERROR_NO_LOCAL_MANIFEST ..... ");
cc.log("No local manifest file found, skip assets update.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
cc.log("enter UPDATE_PROGRESSION ..... ");
that._percent = event.getPercent();
cc.log(that._percent + "%");
var msg = event.getMessage();
if (msg) {
cc.log(msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
cc.log("enter ERROR_DOWNLOAD_MANIFEST ..... ");
cc.log("Fail to download manifest file, update skipped.");
that.loadGame();
break;
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log("enter ERROR_PARSE_MANIFEST ..... ");
cc.log("Fail to download manifest file, update skipped.");
that.loadGame();
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("enter ALREADY_UP_TO_DATE ..... ");
cc.log("ALREADY_UP_TO_DATE.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log("enter UPDATE_FINISHED ..... ");
cc.log("Update finished.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log("enter UPDATE_FAILED ..... ");
cc.log("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_UPDATING:
cc.log("enter ERROR_UPDATING ..... ");
cc.log("Asset update error: " + event.getAssetId() + ", " + event.getMessage());
that.loadGame();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log("enter ERROR_DECOMPRESS ..... ");
cc.log(event.getMessage());
that.loadGame();
break;
default:
break;
}
});
cc.eventManager.addListener(listener, 1);
this._am.update();
cc.director.runScene(this);
}
this.schedule(this.updateProgress, 0.5);
},
loadGame:function(){
cc.log("enter loadGame function ..... ");
//jsList是jsList.js的变量,记录全部js。
cc.loader.loadJs(["src/jsList.js"], function(){
cc.loader.loadJs(jsList, function(){
cc.director.runScene(new HelloWorldScene());
});
});
},
updateProgress:function(dt){
cc.log("enter updateProgress function ..... ");
this._progress.string = "update " + this._percent + "%";
},
onExit:function(){
cc.log("AssetsManager::onExit");
this._am.release();
this._super();
}
});
这里用到了上一步创建的res/project.manifest文件,目的是用于进行版本对比。
因为下载的文件是保存在可写目录下,加载的顺序发生了变化,所以需要修改根目录下的main.js和“project.json”两个文件。
main.js :
cc.game.onStart = function(){
if(!cc.sys.isNative && document.getElementById("cocosLoading")) //If referenced loading.js, please remove it
document.body.removeChild(document.getElementById("cocosLoading"));
// Pass true to enable retina display, on Android disabled by default to improve performance
cc.view.enableRetina(cc.sys.os === cc.sys.OS_IOS ? true : false);
// Adjust viewport meta
cc.view.adjustViewPort(true);
// Uncomment the following line to set a fixed orientation for your game
// cc.view.setOrientation(cc.ORIENTATION_PORTRAIT);
// Setup the resolution policy and design resolution size
cc.view.setDesignResolutionSize(960, 640, cc.ResolutionPolicy.SHOW_ALL);
// The game will be resized when browser size change
cc.view.resizeWithBrowserSize(true);
var scene = new AssetsManagerLoaderScene();
scene.run()
// //load resources
// cc.LoaderScene.preload(g_resources, function () {
// cc.director.runScene(new HelloWorldScene());
// }, this);
};
cc.game.run();
project.json :
{
"project_type": "javascript",
"debugMode" : 1,
"showFPS" : true,
"frameRate" : 60,
"noCache" : false,
"id" : "gameCanvas",
"renderMode" : 0,
"engineDir":"frameworks/cocos2d-html5",
"modules" : ["cocos2d", "extensions"],
"jsList" : [
"src/assetsManagerScene.js"
]
}
到这一步客户端基本配置已经完成,这个时候客户端是可以运行的。
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest",
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"groupVersions" : {
"1" : "1.0.1"
},
"engineVersion" : "3.12"
}
project.manifest :
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest",
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"groupVersions" : {
"1" : "1.0.1"
},
"engineVersion" : "3.12",
"assets" : {
"update1" : {
"path" : "src/app.zip",
"md5" : "D7698389FD1CA121DCD896035D67687C",
"compressed" : true ,
"group" : "1"
}
},
"searchPaths" : [
]
}
packageUrl : 远程资源的下载根路径。
remoteVersionUrl : 远程版本文件的路径,用来判断服务器端是否有新版本的资源。
remoteManifestUrl : 远程配置文件的路径,包含版本信息以及所有资源信息。
version : 配置文件对应的版本。
groupVersions : 是新增的功能字段,用于做增量更新很方便。
engineVersion : 配置文件对应的引擎版本。
assets : 所有资源信息。
key : 升级名称
path : 键代表资源的相对路径(相对于packageUrl)。
md5 : md5值代表资源文件的版本信息。
compressed : [可选项] 如果值为true,文件被下载后会自动被解压,目前仅支持zip压缩格式。
searchPaths : 需要添加到Cocos2d引擎中的搜索路径列表。
通过学习底层逻辑,发现更新的逻辑顺序是这样的:
先通过本地的version.manifest和服务端的version.manifest比较,如果本地没有version.manifest,则会先进行下载,如果本地version低于服务端,那么就会再去获取project.manifest。
如果version相同,那么会比较groupVersions。
如果本地没有下载过groupVersions中的任何更新,那么会依次下载升级包。
如果本地下载过1.0.1版本的升级包,那么就会跳过1.0.1下载属于1.0.2版本的升级内容。
如果下载失败,或者没有网络导致更新失败的,会继续使用未更新前的版本。并且下次启动会继续尝试更新。
这是我的web工程目录
至此,基本的更新逻辑已经完成。
增量更新只是在服务端version.manifest和project.manifest文件增加相应的描述即可。
我在做的过程中主要遇到了一下几个问题:
1. 由于公司内部网络比较多,大家接入的都是内网,导致测试手机和web服务器不在一个网络,这个是个人原因
2. web工程目录问题,导致文件下载失败,错误描述如图:
这个主要原因是,manifest文件中的packageUrl和path拼接后的地址与web工程的目录不符合,建议各位看官在web工程搭建完成之后先在网页访问下需要下载的文件,确保拼接后的地址能过够正常访问。
点我获取源码