每次登陆游戏利用cocos的assetManager从服务器拉去当前最新的两个文件。 一个是version.mainifest,一个project.mainifest. 这两个文件都是xml的描述文件。
一个包含了版本信息,第二个包含了游戏所有资源的MD5码。首先通过version文件对比本地的版本是否相同,如果不相同,再通过跟本地的project文件对比MD5码来判断哪些文件需要重新下载,替换资源。
步骤:
- 有一个文件下载的热更新服务器,将最新项目资源(res/ src/ 目录)放入热更新服务器中,添加版本信息母文件(version_info.json)和python脚本文件eneateManifest.py(生成project.manifest、version.manifest文件)。
2.version_info.json文件: 主要用来配置信息
1 2 3 4 5 6 7 8 9
| { "packageUrl" : "http://ip:port/update/MyProj/assets/", "remoteManifestUrl" : "http://ip:port/update/MyProj/version/project.manifest", "remoteVersionUrl" : "http://ip:port/update/MyProj/version/version.manifest", "engineVersion" : "3.3", "update_channel" : "Android", "bundle" : "2018111701", "version" : "1.0.0", }
|
3.eneateManifest.py文件: 这个文件是一个python。目的是生成对应的version和project文件。project文件可以帮你给每个资源生成独一无二的MD5码,相当于每个资源的标记。下面是一段python文件的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| #coding:utf-8 import os import sys import json import hashlib import subprocess import getpass username = getpass.getuser() # 改变当前工作目录 #os.chdir('/Users/' + username + '/Documents/client/MyProj/') assetsDir = { #MyProj文件夹下需要进行热跟的文件夹 "searchDir" : ["src", "res"], #需要忽略的文件夹 "ignorDir" : ["cocos", "framework", ".svn"], #需要忽略的文件 "ignorFile":[".DS_Store"], } versionConfigFile = "version/version_info.json" #版本信息的配置文件路径 versionManifestPath = "version/version.manifest" #由此脚本生成的version.manifest文件路径 projectManifestPath = "version/project.manifest" #由此脚本生成的project.manifest文件路径 # projectManifestPath = "/Users/ximi/Documents/client/MyProj/res/version/project.manifest" #由此脚本生成的project.manifest文件路径(mac机) class SearchFile: def __init__(self): self.fileList = [] for k in assetsDir: if (k == "searchDir"): for searchdire in assetsDir[k]: self.recursiveDir(searchdire) def recursiveDir(self, srcPath): ''' 递归指定目录下的所有文件''' dirList = [] #所有文件夹 files = os.listdir(srcPath) #返回指定目录下的所有文件,及目录(不含子目录) for f in files: #目录的处理 if (os.path.isdir(srcPath + '/' + f)): if (f[0] == '.' or (f in assetsDir["ignorDir"])): #排除隐藏文件夹和忽略的目录 pass else: #添加非需要的文件夹 dirList.append(f) #文件的处理 elif (os.path.isfile(srcPath + '/' + f)) and (f not in assetsDir["ignorFile"]): self.fileList.append(srcPath + '/' + f) #添加文件 #遍历所有子目录,并递归 for dire in dirList: #递归目录下的文件 self.recursiveDir(srcPath + '/' + dire) def getAllFile(self): ''' get all file path''' return tuple(self.fileList) def CalcMD5(filepath): ""'generate a md5 code by a file path'" with open(filepath,'rb') as f: md5obj = hashlib.md5() md5obj.update(f.read()) return md5obj.hexdigest() def getVersionInfo(): '''get version config data''' configFile = open(versionConfigFile,"r") json_data = json.load(configFile) configFile.close() # json_data["version"] = json_data["version"] + '.' + str(GetSvnCurrentVersion()) json_data["version"] = json_data["version"] return json_data def GenerateVersionManifestFile(): ''' 生成大版本的version.manifest''' json_str = json.dumps(getVersionInfo(), indent = 2) fo = open(versionManifestPath,"w") fo.write(json_str) fo.close() def GenerateProjectManifestFile(): searchfile = SearchFile() fileList = list(searchfile.getAllFile()) project_str = {} project_str.update(getVersionInfo()) dataDic = {} for f in fileList: dataDic[f] = {"md5" : CalcMD5(f)} print f project_str.update({"assets":dataDic}) json_str = json.dumps(project_str, sort_keys = True, indent = 2) fo = open(projectManifestPath,"w") fo.write(json_str) fo.close() if __name__ == "__main__": GenerateVersionManifestFile() GenerateProjectManifestFile()
|
生成version.manifest如下
1 2 3 4 5 6 7
| { "packageUrl": "http://ip:port/update/MyProj/assets/", "engineVersion": "3.3", "version": "1.0.0", "remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest", "remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest" }
|
生成project.manifest如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { "assets": { "src/packages/mvc/init.lua": { "md5": "6b9173481a1300c5e737ad5885ebef00" }, "src/protobuf.lua": { "md5": "f790fe35eb179a4341ff41d94e488a5d" } ... }, "packageUrl": "http://ip:port/update/MyProj/assets/", "engineVersion": "3.3", "version": "1.0.0", "remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest", "remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest" }
|
4.游戏客户端: 利用cocos assetManager来从服务器获取文件并且进行资源的替换(这里所谓的替换并不是真正的替换,利用了Fileutils->searchPath() 设置资源文件读取的优先级。也就是老资源和代码并没有删除,而是舍弃不用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
|
local AssetsManager = class("AssetsManager",function () return cc.LayerColor:create(cc.c4b(20, 20, 20, 220)) end) function AssetsManager:ctor() self:onNodeEvent("exit", handler(self, self.onExitCallback)) self:initUI() self:setAssetsManage() end function AssetsManager:onExitCallback() self.assetsManagerEx:release() end function AssetsManager:initUI() local hintLabel = cc.Label:createWithTTF("正在更新...", CONFIG.TTF_FONT_2, 20) :addTo(self) :move(600, 80) local progressBg = display.newSprite("sprites/hyd_progress_bg.png") :addTo(self) :move(600, 40) self.progress = cc.ProgressTimer:create(display.newSprite("sprites/hyd_progress.png")) :addTo(progressBg) :move(380, 19) self.progress:setType(cc.PROGRESS_TIMER_TYPE_BAR) self.progress:setBarChangeRate(cc.p(1, 0)) self.progress:setMidpoint(cc.p(0.0, 0.5)) self.progress:setPercentage(0) self.listener = cc.EventListenerTouchOneByOne:create() self.listener:setSwallowTouches(true) local onTouchBegan = function (touch, event) return true end self.listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN) cc.Director:getInstance():getEventDispatcher():addEventListenerWithSceneGraphPriority(self.listener, self) end function AssetsManager:setAssetsManage() local storagePath = cc.FileUtils:getInstance():getWritablePath() .. "NewRes/" local resPath = storagePath.. '/res/' local srcPath = storagePath.. '/src/' if not (cc.FileUtils:getInstance():isDirectoryExist(storagePath)) then cc.FileUtils:getInstance():createDirectory(storagePath) cc.FileUtils:getInstance():createDirectory(resPath) cc.FileUtils:getInstance():createDirectory(srcPath) end local searchPaths = cc.FileUtils:getInstance():getSearchPaths() table.insert(searchPaths, 1, storagePath) table.insert(searchPaths, 2, resPath) table.insert(searchPaths, 3, srcPath) cc.FileUtils:getInstance():setSearchPaths(searchPaths) self.assetsManagerEx = cc.AssetsManagerEx:create("version/project.manifest", storagePath) self.assetsManagerEx:retain() local eventListenerAssetsManagerEx = cc.EventListenerAssetsManagerEx:create(self.assetsManagerEx, function (event) self:handleAssetsManagerEvent(event) end) local dispatcher = cc.Director:getInstance():getEventDispatcher() dispatcher:addEventListenerWithFixedPriority(eventListenerAssetsManagerEx, 1) self.assetsManagerEx:update() end function AssetsManager:handleAssetsManagerEvent(event) local eventCodeList = cc.EventAssetsManagerEx.EventCode local eventCodeHand = { [eventCodeList.ERROR_NO_LOCAL_MANIFEST] = function () print("发生错误:本地资源清单文件未找到") end, [eventCodeList.ERROR_DOWNLOAD_MANIFEST] = function () print("发生错误:远程资源清单文件下载失败") self:downloadManifestError() end, [eventCodeList.ERROR_PARSE_MANIFEST] = function () print("发生错误:资源清单文件解析失败") end, [eventCodeList.NEW_VERSION_FOUND] = function () print("发现找到新版本") end, [eventCodeList.ALREADY_UP_TO_DATE] = function () print("已经更新到服务器最新版本") self:updateFinished() end, [eventCodeList.UPDATE_PROGRESSION]= function () print("更新过程的进度事件") self.progress:setPercentage(event:getPercentByFile()) end, [eventCodeList.ASSET_UPDATED] = function () print("单个资源被更新事件") end, [eventCodeList.ERROR_UPDATING] = function () print("发生错误:更新过程中遇到错误") end, [eventCodeList.UPDATE_FINISHED] = function () print("更新成功事件") self:updateFinished() end, [eventCodeList.UPDATE_FAILED] = function () print("更新失败事件") end, [eventCodeList.ERROR_DECOMPRESS] = function () print("解压缩失败") end } local eventCode = event:getEventCode() if eventCodeHand[eventCode] ~= nil then eventCodeHand[eventCode]() end end function AssetsManager:updateFinished() self:setVisible(false) self.listener:setEnabled(false) end function AssetsManager:downloadManifestError() self:setVisible(false) self.listener:setEnabled(false) end return AssetsManager
|
Android apk 安装后在手机中还是以apk存在,apk 不可写入和删除,所以热更新下载的最新资源都存在缓存中,并添加缓存目录为最高优先级搜索目录,加载资源时从最高优先级目录中加载从而起到替换更新的作用。cocos2dx中有一个热更新类AssetsManagerEx,用这个类实现热更功能时需要有两个文件,project.manifest以及version.manifest。这里主要是project.manifest文件
Cocos自身也封装了热更新的模块AssetsManager、AssetsManagerEx。
AssetsManager采用的是升级包的管理方式,首先进行版本号对比,然后根据URL获取对应的升级包,解压升级包,设置资源加载路径,通过加载writepath目录下最新文件的方式来实现更新。问题是当涉及跳版本更新,或只有一个文件被改动时,用户就要下载前面全部的升级内容,升级包会越来越大。
AssetsManagerEx是AssetsManager的加强版,不同的是不再使用升级包的方式,而是采用单个文件拉取的方式。首先获取本地更新配置,之后与服务器的更新配置比对,得出差异文件,之后单个拉取差异文件。当本地版本大于服务器版本时,会清理掉本地更新缓存。AssetsManagerEx也有尚未解决的问题,例如多个更新序列无法并行,只能顺序启动。另外版本后期随着项目庞大配置文件几乎包含了所有的文件信息,对比文件时间的耗时会越来越长。