注:本文基于Quick-cocos2dx-3.3版本编写php
lua相对于c++开发的优势之一是代码能够在运行的时候才加载,基于此咱们不只能够在编写的时候热更新代码(针对开发过程当中的热更新将在另一篇文章中介绍),也能够在版本已经发布以后更新代码。html
cocos2dx的热更新已经有不少篇文章介绍了,这里主要是基于 quick-cocos2d-x的热更新机制实现(终极版2)(更新3.3版本)基础上修改。node
launcher模块的具体介绍能够看原文,不过这里的更新逻辑是稍微修改了的。
先请求服务器的launcher模块文件,若是本地launcher模块文件和服务器不一样则替换新的模块再从新加载。
具体更新逻辑如流程图所示:python
原文是把文件md5和版本信息都放在同一个文件里面,这里把版本信息和文件md5分红两个文件,这样的好处是不用每次都把完整的md5文件列表下载下来。除此以外还增长了程序版本号判断,优化了一些逻辑。具体代码见最后的资源连接。android
原文的md5信息(flist)是经过lua代码调用引擎模块生成,可是鉴于工程太大不利于分享(其实目的只是要生成文件md5信息),因此这里把代码改为python版本的了。ios
注意,若是你也想要尝试把lua改为其余语言实现,你可能会发现生成的md5和lua版本的不一样,这是由于lua版本将字节流转换成大写的十六进制来生成md5的。c++
#lua 版本 local function hex(s) s=string.gsub(s,"(.)",function (x) return string.format("%02X",string.byte(x)) end) return s end #python 版本 def toHex(s): return binascii.b2a_hex(s).upper()
具体的python脚本代码(仍是基于上个教程的脚本增长代码)git
#coding=utf-8 #!/usr/bin/python import os import os.path import sys, getopt import subprocess import shutil import time, datetime import platform from hashlib import md5 import hashlib import binascii def removeDir(dirName): if not os.path.isdir(dirName): return filelist=[] filelist=os.listdir(dirName) for f in filelist: filepath = os.path.join( dirName, f ) if os.path.isfile(filepath): os.remove(filepath) elif os.path.isdir(filepath): shutil.rmtree(filepath,True) def copySingleFile(sourceFile, targetFile): if os.path.isfile(sourceFile): if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))): open(targetFile, "wb").write(open(sourceFile, "rb").read()) def copyFiles(sourceDir, targetDir, isAll): for file in os.listdir(sourceDir): sourceFile = os.path.join(sourceDir, file) targetFile = os.path.join(targetDir, file) if os.path.isfile(sourceFile): if not isAll: extName = file.split('.', 1)[1] if IgnoreCopyExtFileDic.has_key(extName): continue if not os.path.exists(targetDir): os.makedirs(targetDir) if not os.path.exists(targetFile) or(os.path.exists(targetFile) and (os.path.getsize(targetFile) != os.path.getsize(sourceFile))): open(targetFile, "wb").write(open(sourceFile, "rb").read()) if os.path.isdir(sourceFile): First_Directory = False copyFiles(sourceFile, targetFile, isAll) def toHex(s): return binascii.b2a_hex(s).upper() def md5sum(fname): def read_chunks(fh): fh.seek(0) chunk = fh.read(8096) while chunk: yield chunk chunk = fh.read(8096) else: #最后要将游标放回文件开头 fh.seek(0) m = hashlib.md5() if isinstance(fname, basestring) and os.path.exists(fname): with open(fname, "rb") as fh: for chunk in read_chunks(fh): m.update(toHex(chunk)) #上传的文件缓存 或 已打开的文件流 elif fname.__class__.__name__ in ["StringIO", "StringO"] or isinstance(fname, file): for chunk in read_chunks(fname): m.update(toHex(chunk)) else: return "" return m.hexdigest() def calMD5ForFolder(dir): md5Dic = [] folderDic = {} for root, subdirs, files in os.walk(dir): #get folder folderRelPath = os.path.relpath(root, dir) if folderRelPath != '.' and len(folderRelPath) > 0: normalFolderPath = folderRelPath.replace('\\', '/') #convert to / path folderDic[normalFolderPath] = True #get md5 for fileName in files: filefullpath = os.path.join(root, fileName) filerelpath = os.path.relpath(filefullpath, dir) size = os.path.getsize(filefullpath) normalPath = filerelpath.replace('\\', '/') #convert to / path if IgnoreMd5FileDic.has_key(fileName): #ignode special file continue print normalPath md5 = md5sum(filefullpath) md5Dic.append({'name' : normalPath, 'code' : md5, 'size' : size}) print 'MD5 figure end' return md5Dic, folderDic #------------------------------------------------------------------- def initEnvironment(): #注意:复制的资源分两种 #第一种是加密的资源,从packres目录复制到APP_RESOURCE_ROOT。加密资源的类型在PackRes.php的whitelists定义。 #第二种是普通资源,从res目录复制到APP_RESOURCE_ROOT。IgnoreCopyExtFileDic定义了不复制的文件类型(一、加密资源,如png文件;二、无用资源,如py文件) global ANDROID_APP_VERSION global IOS_APP_VERSION global ANDROID_VERSION global IOS_VERSION global BOOL_BUILD_APP #是否构建app global APP_ROOT #工程根目录 global APP_ANDROID_ROOT #安卓根目录 global QUICK_ROOT #引擎根目录 global QUICK_BIN_DIR #引擎bin目录 global APP_RESOURCE_ROOT #生成app的资源目录 global APP_RESOURCE_RES_DIR #资源目录 global IgnoreCopyExtFileDic #不从res目录复制的资源 global IgnoreMd5FileDic #不计算md5的文件名 global APP_BUILD_USE_JIT #是否使用jit global PHP_NAME #php global SCRIPT_NAME #scriptsName global BUILD_PLATFORM #生成app对应的平台 BOOL_BUILD_APP = True IgnoreCopyExtFileDic = { 'jpg' : True, 'png' : True, 'tmx' : True, 'plist' : True, 'py' : True, } IgnoreMd5FileDic = { '.DS_Store' : True, 'version' : True, 'flist' : True, 'launcher.zip' : True, '.' : True, '..' : True, } SYSTEM_TYPE = platform.system() APP_ROOT = os.getcwd() APP_ANDROID_ROOT = APP_ROOT + "/frameworks/runtime-src/proj.android" QUICK_ROOT = os.getenv('QUICK_V3_ROOT') if QUICK_ROOT == None: print "QUICK_V3_ROOT not set, please run setup_win.bat/setup_mac.sh in engine root or set QUICK_ROOT path" return False if(SYSTEM_TYPE =="Windows"): QUICK_BIN_DIR = QUICK_ROOT + "quick/bin" PHP_NAME = QUICK_BIN_DIR + "/win32/php.exe" #windows BUILD_PLATFORM = "android" #windows dafault build android SCRIPT_NAME = "/compile_scripts.bat" else: PHP_NAME = "php" BUILD_PLATFORM = "ios" #mac default build ios QUICK_BIN_DIR = QUICK_ROOT + "/quick/bin" #mac add '/' SCRIPT_NAME = "/compile_scripts.sh" if(BUILD_PLATFORM =="ios"): APP_BUILD_USE_JIT = False #ios not use jit if BOOL_BUILD_APP: APP_RESOURCE_ROOT = APP_ROOT + "/Resources" APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res" else: APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp" APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT else: APP_BUILD_USE_JIT = True if BOOL_BUILD_APP: APP_RESOURCE_ROOT = APP_ANDROID_ROOT + "/assets" #default build android APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT + "/res" else: APP_RESOURCE_ROOT = APP_ROOT + "/server/game/cocos2dx/udp" APP_RESOURCE_RES_DIR = APP_RESOURCE_ROOT print 'App root: %s' %(APP_ROOT) print 'App resource root: %s' %(APP_RESOURCE_ROOT) return True def svnUpdate(): print "1:svn update" try: args = ['svn', 'update'] proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT) while proc.poll() == None: print proc.stdout.readline(), print proc.stdout.read() except Exception,e: print Exception,":",e def packRes(): print "2:pack res files" removeDir(APP_ROOT + "/packres/") #--->删除旧加密资源 scriptName = QUICK_BIN_DIR + "/lib/pack_files.php" try: args = [PHP_NAME, scriptName, '-c', 'PackRes.php'] proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT) while proc.poll() == None: print proc.stdout.readline(), print proc.stdout.read() except Exception,e: print Exception,":",e def copyResourceFiles(): print "3:copy resource files" print "remove old resource files" removeDir(APP_RESOURCE_ROOT) if not os.path.exists(APP_RESOURCE_ROOT): print "create resource folder" os.makedirs(APP_RESOURCE_ROOT) if BOOL_BUILD_APP: #copy all resource print "copy config" copySingleFile(APP_ROOT + "/config.json", APP_RESOURCE_ROOT + "/config.json") copySingleFile(APP_ROOT + "/channel.lua", APP_RESOURCE_ROOT + "/channel.lua") print "copy src" copyFiles(APP_ROOT + "/scripts/", APP_RESOURCE_ROOT + "/src/", True) print "copy res" copyFiles(APP_ROOT + "/res/", APP_RESOURCE_RES_DIR, False) print "copy pack res" copyFiles(APP_ROOT + "/packres/", APP_RESOURCE_RES_DIR, True) def compileScriptFile(compileFileName, srcName, compileMode): scriptDir = APP_RESOURCE_RES_DIR + "/code/" if not os.path.exists(scriptDir): os.makedirs(scriptDir) try: scriptsName = QUICK_BIN_DIR + SCRIPT_NAME srcName = APP_ROOT + "/" + srcName outputName = scriptDir + compileFileName args = [scriptsName,'-i',srcName,'-o',outputName,'-e',compileMode,'-es','XXTEA','-ek','ilovecocos2dx'] if APP_BUILD_USE_JIT: args.append('-jit') proc = subprocess.Popen(args, shell=False, stdout = subprocess.PIPE, stderr=subprocess.STDOUT) while proc.poll() == None: outputStr = proc.stdout.readline() print outputStr, print proc.stdout.read(), except Exception,e: print Exception,":",e def compileFile(): print "4:compile script file" compileScriptFile("game.zip", "src", "xxtea_zip") #--->代码加密 compileScriptFile("launcher.zip", "pack_launcher", "xxtea_zip") #--->更新模块加密 def writeFile(fileName, strArr): if os.path.isfile(fileName): print "Remove old file!" os.remove(fileName) #write file f = file(fileName, 'w') for _, contentStr in enumerate(strArr): f.write(contentStr) f.close() def genFlist(): print "5: generate flist" # flist文件格式 lua table # key # --> dirPaths 目录 # --> fileInfoList 文件名,md5,size folderPath = APP_RESOURCE_RES_DIR md5Dic, folderDic = calMD5ForFolder(folderPath) #sort md5 sortMd5Dic = sorted(md5Dic, cmp=lambda x,y : cmp(x['name'], y['name'])) #convert folder dic to arr folderNameArr = [] for folderName, _ in folderDic.iteritems(): folderNameArr.append(folderName) #sort folder name sortFolderArr = sorted(folderNameArr, cmp=lambda x,y : cmp(x, y)) #str arr generate strArr = [] strArr.append('local flist = {\n') #dirPaths strArr.append('\tdirPaths = {\n') for _,folderName in enumerate(sortFolderArr): strArr.append('\t\t{name = "%s"},\n' % folderName) strArr.append('\t},\n') #fileInfoList strArr.append('\tfileInfoList = {\n') for index, md5Info in enumerate(sortMd5Dic): name = md5Info['name'] code = md5Info['code'] size = md5Info['size'] strArr.append('\t\t{name = "%s", code = "%s", size = %d},\n' % (name, code, size)) strArr.append('\t},\n') strArr.append('}\n') strArr.append('return flist\n') writeFile(folderPath + "/flist", strArr) def genVersion(): print "6: generate version" folderPath = APP_RESOURCE_RES_DIR #str arr generate strArr = [] strArr.append('local version = {\n') strArr.append('\tandroidAppVersion = %d,\n' % ANDROID_APP_VERSION) strArr.append('\tiosAppVersion = %d,\n' % IOS_APP_VERSION) strArr.append('\tandroidVersion = "%s",\n' % ANDROID_VERSION) strArr.append('\tiosVersion = "%s",\n' % IOS_VERSION) strArr.append('}\n') strArr.append('return version\n') writeFile(folderPath + "/version", strArr) if __name__ == '__main__': print 'Pack App start!--------->' isInit = initEnvironment() if isInit == True: #若不更新资源则直接执行copyResourceFiles和compileScript svnUpdate() #--->更新svn packRes() #--->资源加密(若资源如图片等未更新则此步可忽略) copyResourceFiles() #--->复制res资源 compileFile() #--->lua文件加密 genFlist() #--->生成flist文件 ANDROID_APP_VERSION = 1 #app 更新版本才须要更改 IOS_APP_VERSION = 1 #app 更新版本才须要更改 ANDROID_VERSION = "1.0.1" IOS_VERSION = "1.0.1" genVersion() #--->生成version文件 print '<---------Pack App end!'
注意:这个脚本是集成代码加密、资源加密、热更新文件生成的。具体使用的时候确定会遇到不少坑的github
坑1:项目使用luajit。
热更新和luajit有点不完美适应,由于iOS的luajit是2.1beta的(iOS的坑),而其余平台是使用的是旧版本luajit,这意味着它们的更新文件不能通用,iOS和android下载服务器的加密代码要区分开,固然若是项目没有用luajit的话就没有这个烦恼了。shell
坑2: 资源文件的位置。
android/iOS的文件引用时注意不要把未加密的代码复制进去了,上面的pyhton脚本已经帮你作了部分操做了,可是还有一些须要本身手动去改。
iOS:xcode工程注意要把原来的资源引用换成加密的资源(Mac下执行脚本会把假面资源拷贝到Resource目录下)
Android:若是你是用build_apk、build_native、build_native_release来编译的话,注意把proj.android里面的build_native_release脚本的资源复制删除语句屏蔽掉
windows:由于windows是开发的时候才用,因此是直接引用源代码的。不过你要发布windows版本的话,须要自行替换加密资源了。
坑3:检查脚本是否放在正确的位置。
python脚本/PackRes.php放在工程根目录(res、src同级目录);FilesPacker.php/pack_files.php放在引擎相应目录;
坑4:检查QUICK_ROOT是否已经设置。
由于脚本要用到引擎自带的加密脚本,注意Mac使用的shell命令(.sh文件)有权限执行
坑5:检查参数是否正确设置。
python脚本中,APP_BUILD_USE_JIT是否使用luajit加密脚本,BOOL_BUILD_APP是否打包apk仍是热更新(复制的目录不一样)
由于代码已经加密,并且加入了热更新模块,因此lua的加载入口须要修改。
首先找到AppDelegate.cpp文件,加入初始化资源搜索路径initResourcePath方法,而后增长更新文件和加密文件判断。
这里有三种状况。
1:更新模式(发布版本使用)
2:加密模式(无更新,windows版本使用)
3:普通模式(无更新和无加密,开发时候使用)
void AppDelegate::initResourcePath() { FileUtils* sharedFileUtils = FileUtils::getInstance(); std::string strBasePath = sharedFileUtils->getWritablePath(); #if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)|| (CC_TARGET_PLATFORM == CC_PLATFORM_IOS) sharedFileUtils->addSearchPath("res/"); #else sharedFileUtils->addSearchPath("../../res/"); #endif sharedFileUtils->addSearchPath(strBasePath + "upd/", true); } bool AppDelegate::applicationDidFinishLaunching() { #if CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 initRuntime(); #elif (COCOS2D_DEBUG > 0 && CC_CODE_IDE_DEBUG_SUPPORT > 0) // NOTE:Please don't remove this call if you want to debug with Cocos Code IDE if (_launchMode) { initRuntime(); } #endif //add resource path initResourcePath(); // initialize director auto director = Director::getInstance(); auto glview = director->getOpenGLView(); if(!glview) { Size viewSize = ConfigParser::getInstance()->getInitViewSize(); string title = ConfigParser::getInstance()->getInitViewName(); #if (CC_TARGET_PLATFORM == CC_PLATFORM_WIN32 || CC_TARGET_PLATFORM == CC_PLATFORM_MAC) extern void createSimulator(const char* viewName, float width, float height, bool isLandscape = true, float frameZoomFactor = 1.0f); bool isLanscape = ConfigParser::getInstance()->isLanscape(); createSimulator(title.c_str(),viewSize.width,viewSize.height, isLanscape); #else glview = cocos2d::GLViewImpl::createWithRect(title.c_str(), Rect(0, 0, viewSize.width, viewSize.height)); director->setOpenGLView(glview); #endif director->startAnimation(); } auto engine = LuaEngine::getInstance(); ScriptEngineManager::getInstance()->setScriptEngine(engine); lua_State* L = engine->getLuaStack()->getLuaState(); lua_module_register(L); // use Quick-Cocos2d-X quick_module_register(L); LuaStack* stack = engine->getLuaStack(); stack->setXXTEAKeyAndSign("ilovecocos2dx", strlen("ilovecocos2dx"), "XXTEA", strlen("XXTEA")); stack->addSearchPath("src"); FileUtils *utils = FileUtils::getInstance(); //1: try to load launcher module const char *updateFileName = "code/launcher.zip"; std::string updateFilePath = utils->fullPathForFilename(updateFileName); bool isUpdate = false; if (updateFilePath.compare(updateFileName) != 0) //check if update file exist { printf("%s\n", updateFilePath.c_str()); isUpdate = true; engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str()); } if (!isUpdate) //no update file { //2: try to load game script module const char *zipFilename ="code/game.zip"; std::string zipFilePath = utils->fullPathForFilename(zipFilename); if (zipFilePath.compare(zipFilename) == 0) //no game zip file use default lua file { engine->executeScriptFile(ConfigParser::getInstance()->getEntryFile().c_str()); } else { //3: default load game script stack->loadChunksFromZIP(zipFilename); stack->executeString("require 'main'"); } } return true; }
对于热更新,游戏执行后首先执行main.lua的代码,main.lua再调用launcher模块的代码,launcher根据版本状况决定接下来的逻辑。
这里的main.lua放在script目录里,执行python脚本后main.lua会复制到对应的src目录下
//main.lua function __G__TRACKBACK__(errorMessage) print("----------------------------------------") print("LUA ERROR: " .. tostring(errorMessage) .. "\n") print(debug.traceback("", 2)) print("----------------------------------------") end local fileUtils = cc.FileUtils:getInstance() fileUtils:setPopupNotify(false) -- 清除fileCached 避免没法加载新的资源。 fileUtils:purgeCachedEntries() cc.LuaLoadChunksFromZIP("code/launcher.zip") package.loaded["launcher.launcher"] = nil require("launcher.launcher")