注:本文基於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")