感谢博毅创为的Blake老师,他手把手的讲课让我明白了让我困扰以久的热更新问题,Blake老师是真正的实干家,比起官网那些云里雾里的又运行不起来教程靠谱的多,原理机制也讲得明明白白,细节上的问题也一一讲解了,如果不是Blake老师,我恐怕一辈子也搞不定热更新这个成为我梦魇的问题。
关于cocosCreator的热更新,官方和面试时热更新原理我们都背的滚瓜烂熟了,就是把要更新的文件放在服务器上,游戏开始时比较服务器上的文件MD5码和游戏本地的MD5码,如果不一样就下载。
可是这有几个关键问题要搞明白:
1.究竟要哪些资源是热更新的目标资源,我们要更新的无外乎是代码和图片等资源,我们平时编辑的图片呀代码呀都是在creator编辑器里编辑的,那么放服务器的图片,代码什么的是不是要跟creator的assets的目录结构和命名一样呢,这样你可就大错特错了。
2. 更新下来的资源,要游戏怎么认出来并且使用热更新下来的资源呢,你会说设置搜索路径呀!这话说的轻巧,等到你用的时候你会发现为什么游戏死活都认不出你设的搜索路径,到底问题出在哪里呢?这就是理论和实践中的巨大差异,如果不把这些实际的细节问题解决,想实现热更新永远只是水中花雾中月,让你分分钟想弃坑。
先解决第一个问题,我们实际游戏运行的资源和代码到底在哪里,如果这个问题都不明白稀里糊涂的那就啥也别搞了,首先我们的编辑器的资源如下图:
嗯嗯,目录和命名看起来很规整,那我们是不是就把这个assets下的res这目录直接拷到服务器上就行了呢,这样做就大错特错了,实际游戏运行用到的资源跟这个半毛钱关系都没有。为了得到真正的游戏内目录结构,需要打包。我们为了操作调试方便,采用windows打包,安卓打包的目录和这个差不多。
选择项目构建,选windows构建,link模式(这是为了快一些,实际还要default比较好),如图:
构建成功后就可以到build目录->jsb-link目录->frameworks->runtime-src->proj.win32下,打开VS工程 bycw_client.sln 我是用creator 2.4.3,所以工程用的是VS2015
打开VS编译后进入Debug.win32文件夹,可看到 assets和src两个文件夹,assets是资源,src是代码,我的是2.4.3版本,如果低版本的是res和src目录,这两个目录是需要放到服务器上的
你的资源和写的大部分代码都在这个assets目录下了,自己可以点进去看看
我们需要在服务器上用node.js建一个web服务器,存放这个assets和src,并写一个脚本,遍历里面所有的文件,记录下路径,文件名和md5码
webserver较为简单,用express写一个最简服务器即可:
var express = require("express");
var app = express();
var host = game_config.webserver.host;
var port = game_config.webserver.port;
//设置跨域访问
app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
//把服务器下的www_root当作webserver的工作目录
if (fs.existsSync("www_root")) {
app.use(express.static(path.join(process.cwd(), "www_root")));
}
else {
log.warn("www_root is not exists!!!!!!!!!!!");
}
log.info("webserver started at port ", host, port);
app.listen(port);
webserver下有一个www_root作为工作目录,我们在里面建一个hotupdate目录作为热更新文件夹,把要热更新的资源放在里面,如下图:
最关键的代码来了,我们要递归遍历hotupdate文件夹内容,生成一个json表,记录下来所有文件的文件名,目录结构,md5码,便于和客户端比较,代码如下:
var fs = require("fs");
var path = require('path');
var crypto = require('crypto');
//判断热更新目录下有没有热更新文件
if (!fs.existsSync("./www_root/hotupdate")) {
console.log("hotupdate foled not found");
return;
}
var file_num = 0;
function readDir(dir, obj) {
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
return;
}
var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
for (var i = 0; i < subpaths.length; ++i) {
if (subpaths[i][0] === '.') {
continue;
}
subpath = path.join(dir, subpaths[i]);
stat = fs.statSync(subpath);
if (stat.isDirectory()) {
readDir(subpath, obj);
}
else if (stat.isFile()) {
// Size in Bytes
size = stat['size'];
md5 = crypto.createHash('md5').update(fs.readFileSync(subpath)).digest('hex');
compressed = path.extname(subpath).toLowerCase() === '.zip';
// relative = path.relative(src, subpath);
relative = subpath;
relative = relative.replace(/\\/g, '/');
relative = encodeURI(relative);
out_dir = dir.replace(/\\/g, '/');
obj[relative] = {
'md5' : md5,
'file': relative,
'dir': out_dir,
};
file_num ++;
if (compressed) {
obj[relative].compressed = true;
}
}
}
}
var obj = {};
process.chdir("./www_root");
//readDir("hotupdate/res", obj); //assets
readDir("hotupdate/assets", obj); //读取热更新目录下的assets文件夹
readDir("hotupdate/src", obj); //读取热更新目录下的src文件夹,可根据你自己的情况改路径名
console.log(obj);
var str = JSON.stringify(obj);
fs.writeFile("./hotupdate/hotupdate.json", str, function(err){});
str = "var hotupdate = \n" + str + "\nmodule.exports = hotupdate";
fs.writeFile("./hotupdate/hotupdate.js", str, function(err){});
好了大功告成,我们写个脚本来运行它 build_hotupdate.bat,如下:
node ./apps/webserver/hotupdate.js
pause;
只要双击运行它就能得到一个热更新的文件列表脚本,在hotupdate目录下,名为hotupdate.json,我们可以用文本编辑器打开,或浏览器里打开,如下:
可看到每个文件名都为一个key,key下为三个键值 md5, file, dir分别为你想要的md5码,文件结构和文件名,要好好利用这个文件。
下面是重头戏热更新模块,我们要在游戏登录场景下建一个空结点,名为checkupdate,下面再加个黑色半透明遮罩和标签,看起来像是正儿八经的热更新。并挂个checkupdate.js脚本,如下图
checkhotupdate代码如下:
// Learn TypeScript:
// - https://docs.cocos.com/creator/manual/en/scripting/typescript.html
// Learn Attribute:
// - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html
import {http} from "../../modules/http";
const {ccclass, property} = cc._decorator;
@ccclass
export default class checkhotupdate extends cc.Component {
@property
url: string = 'http://127.0.0.1:10001';
_storagePath:string = "";
hotpath:string = "";
// LIFE-CYCLE CALLBACKS:
onLoad () {
}
set_hotupdate_search_path() {
var path:string[] = jsb.fileUtils.getSearchPaths();
var write_path:string = this._storagePath;
var hotpath = write_path + "\hotupdate";
if(!jsb.fileUtils.isDirectoryExist(hotpath)) {
jsb.fileUtils.createDirectory(hotpath);
}
path.unshift(hotpath);
// 把热更新的path放到最前面
jsb.fileUtils.setSearchPaths(path);
this.hotpath = hotpath;
}
//从本地获取要下载的热更新列表
local_hotupdate_download_list(hotpath) {
var json = {};
var str;
//热更新之后会在hotpath目录下载这个hotupdate.json热更新文件
if (jsb.fileUtils.isFileExist(hotpath + "/hotupdate.json")) {
console.log("存在 isFileExist hotupdate.json")
str = jsb.fileUtils.getStringFromFile(hotpath + "/hotupdate.json");
json = JSON.parse(str);
}
else { // 每次打包这个把本的时候,你都要带上hotupdate.json, 放在 Android assets目录下
console.log("不存在 notexist hotupdate.json")
str = jsb.fileUtils.getStringFromFile("hotupdate.json");
json = JSON.parse(str);
}
return json;
}
download_item(write_path, server_item, end_func) {
console.log("download_item write_path:", write_path, "server_item:",server_item.file);
if (server_item.file.indexOf(".json") >= 0) { //如果扩展名是json,就调用http.get方法
http.get(this.url, "/" + server_item.file, null, function(err, data) {
if (err) {
if (end_func) {
end_func();
}
return;
}
{
var dir_array = new Array(); //定义一数组
dir_array = server_item.dir.split("/");
var walk_dir = write_path;
for(var j = 0; j < dir_array.length; j ++) {
walk_dir = walk_dir + "/" + dir_array[j];
if (!jsb.fileUtils.isDirectoryExist(walk_dir)) {
jsb.fileUtils.createDirectory(walk_dir); //在可写目录下,一级一级的目录建立起下载清单上文件的目录结构
}
}
jsb.fileUtils.writeStringToFile(data, write_path + "/" + server_item.file);
}
if (end_func) {
end_func();
}
});
}
else {
http.download(this.url, "/" + server_item.file, null, function(err, data) {
if (err) {
if (end_func) {
end_func();
}
return;
}
{
var dir_array = new Array(); //定义一数组
dir_array = server_item.dir.split("/");
var walk_dir = write_path;
for(var j = 0; j < dir_array.length; j ++) {
walk_dir = walk_dir + "/" + dir_array[j];
if (!jsb.fileUtils.isDirectoryExist(walk_dir)) {
jsb.fileUtils.createDirectory(walk_dir);
}
}
//jsb.fileUtils.writeDataToFile(data, write_path + "/" + server_item.file);
//jsb.fileUtils.writeToFile
jsb.fileUtils.writeDataToFile(data, write_path + "/" + server_item.file);
}
if (end_func) {
end_func();
}
});
}
}
start () {
this._storagePath = jsb.fileUtils.getWritablePath();
// 设置一下搜索路径
this.set_hotupdate_search_path();
// end
console.log("this.hotpath:", this.hotpath);
//本地的文件列表
var now_list = this.local_hotupdate_download_list(this.hotpath);
var server_list = null;
http.get(this.url, "/hotupdate/hotupdate.json", null, function(err, data) {
if (err) {
this.node.removeFromParent();
return;
}
//服务器上的文件列表
server_list = JSON.parse(data);
var i = 0;
var download_array = [];
for(var key in server_list) {
if(key === "hotupdate/assets/main/native/58/584010d5-455b-4dee-870d-4995cda27157.853e2.jpg")
{
console.log("jpg server md5:",server_list[key].md5);
console.log("jpg local md5:",now_list[key].md5);
}
//本地列表和服务器列表上md5一样的就跳过,不一样的加入到下载列表
if (now_list[key] && now_list[key].md5 === server_list[key].md5) { // 无需更新
continue;
}
download_array.push(server_list[key]);
}
if (download_array.length <= 0) {
console.log("下载列表为空");
this.node.removeFromParent();
return;
}
console.log("下载列表不为空");
var i = 0;
var callback = function() {
i ++;
if (i >= download_array.length) {
jsb.fileUtils.writeStringToFile(data, this.hotpath + "/hotupdate.json");
this.node.removeFromParent();
console.log("game restart 游戏重启")
cc.audioEngine.stopAll();
cc.game.restart();
return;
}
this.download_item(this._storagePath, download_array[i], callback);
}.bind(this);
this.download_item(this._storagePath, download_array[i], callback);
}.bind(this));
}
// update (dt) {}
}
start方法先获取本地的可读写目录,设置下搜索路径
// 设置一下搜索路径
this.set_hotupdate_search_path();
然后获取本地的文件列表 var now_list = this.local_hotupdate_download_list(this.hotpath);
然后通过http.get方法请求服务器上的文件列表
http.get(this.url, "/hotupdate/hotupdate.json", null ....
server_list = JSON.parse(data); //获取服务器列表
然后一个文件一个文件比较本地和服务器上的Md5,不一样的加入下载列表
if (now_list[key] && now_list[key].md5 === server_list[key].md5)
然后调用download_item方法一个文件一个文件下载到本地。
客户端注意,首次打包时要把服务器上最新的hotupdate.json打进包里,因为代码第一步就是先找有没有热更新的hotupdate.json,没有就找本地原始的hotupdate.json,两个都没有无法热更新,如下:
windows环境下热更新下载的文件会在系统盘的可写目录下,我的是: D:\myWorks\BYCW\bycw_server\www_root\hotupdate下
点进去看,会发现你要更新的图片和资源都一级一级目录照本地游戏打包出来的目录结构也生成了,非常方便了,那问题来了,我们怎么让游戏识别出下载下来的文件并使用它而不是包里已经有的老资源呢?
刚才代码第一句就是设置搜索路径,如下图:
好我们实验一下,我们到服务器里的hotupdate,修改下log图片
然后运行一下 服务器上刚才写好的build_hotupdate.bat, 重新生成最新的文件列表,这样客户端登录时就会检测到热更新,并进行下载更新了,但情况好像不是这样
注:为了便于调试,客户端的作法是将刚才客户端下的WIN工程里的Debug.win32目录整个拷出来,因为里面已经 有了编译所需要的dll文件,不会报错是,拷出来改个目录名叫game_release,如下图:
运行出来 正在检测,出现了下载列表,下载完了把热更新结点删除,重新进入游戏,一切好像正常,可是重新进入游戏后Logo图片还是原样,没有任何变化
我们到c盘那个可写目录下的hotupdate目录下看,修改后的图片确实下载下来了,可是好像游戏并没有使用这张图片,明显是搜索路径没有生效,这该 怎么办呢?为什么客户端代码里设置搜索路径不起效的原因我不明白,但是Blake老师说还是要修改下启动main.js代码,加一个设置搜索路径,就好了
function set_hotupdate_search_path() {
var path = jsb.fileUtils.getSearchPaths();
var write_path = jsb.fileUtils.getWritablePath();
var hotpath = write_path + "/hotupdate";
if (!jsb.fileUtils.isDirectoryExist(hotpath)) {
jsb.fileUtils.createDirectory(hotpath);
}
path.unshift(hotpath);
// 把热更新的path放到最前面
jsb.fileUtils.setSearchPaths(path);
}
然后再运行,OK,就可以成功热更新了
完整代码 https://download.csdn.net/download/qiou2719/15123291
服务器webserver可运行 bycw_server下的6start_webserver.bat即可启动服务器看效果了,前提是要装Node.js,服务器地址是 http://127.0.0.1:10001/hotupdate/hotupdate.json