zoukankan      html  css  js  c++  java
  • [cocos4.0 + lua]热更新

    原理:

    每次登陆游戏利用cocos的assetManager从服务器拉去当前最新的两个文件。 一个是version.mainifest,一个project.mainifest. 这两个文件都是xml的描述文件。一个包含了版本信息,第二个包含了游戏所有资源的MD5码。首先通过version文件对比本地的版本是否相同,如果不相同,再通过跟本地的project文件对比MD5码来判断哪些文件需要重新下载,替换资源。

    步骤:

    1. 有一个文件下载的热更新服务器,将最新项目资源(res/ src/ 目录)放入热更新服务器中,添加版本信息母文件(version_info.json)和python脚本文件eneateManifest.py(生成project.manifest、version.manifest文件)。

    2.version_info.json文件: 主要用来配置信息

    {
        "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文件的代码。

    #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如下

    {
      "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如下

    {
        "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() 设置资源文件读取的优先级。也就是老资源和代码并没有删除,而是舍弃不用。

    
    --region *.lua
    --Date
     
    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
     
     
    --endregion
    

    Android apk 安装后在手机中还是以apk存在,apk 不可写入和删除,所以热更新下载的最新资源都存在缓存中,并添加缓存目录为最高优先级搜索目录,加载资源时从最高优先级目录中加载从而起到替换更新的作用。

    cocos2dx中有一个热更新类AssetsManagerEx,用这个类实现热更功能时需要有两个文件,project.manifest以及version.manifest。这里主要是project.manifest文件

    Cocos自身也封装了热更新的模块AssetsManager、AssetsManagerEx。

    AssetsManager采用的是升级包的管理方式,首先进行版本号对比,然后根据URL获取对应的升级包,解压升级包,设置资源加载路径,通过加载writepath目录下最新文件的方式来实现更新。问题是当涉及跳版本更新,或只有一个文件被改动时,用户就要下载前面全部的升级内容,升级包会越来越大。

    AssetsManagerEx是AssetsManager的加强版,不同的是不再使用升级包的方式,而是采用单个文件拉取的方式。首先获取本地更新配置,之后与服务器的更新配置比对,得出差异文件,之后单个拉取差异文件。当本地版本大于服务器版本时,会清理掉本地更新缓存。AssetsManagerEx也有尚未解决的问题,例如多个更新序列无法并行,只能顺序启动。另外版本后期随着项目庞大配置文件几乎包含了所有的文件信息,对比文件时间的耗时会越来越长。



    原文链接:https://www.jianshu.com/p/1d4df0aa4347

  • 相关阅读:
    Spring Cloud 五 Feign的文件上传实现
    Spring Cloud 四:服务消费(Feign)【Dalston版】
    Spring Cloud 三:服务消费(Ribbon)【Dalston版】
    Spring Cloud二:服务消费(基础)【Dalston版】
    Spring Cloud 一:服务注册与发现(Eureka)【Dalston版】
    mybatis 单个参数
    医院设置(图论Floyd)
    Mysql学习——安装登录
    字母游戏(搜索)
    子数列连续和
  • 原文地址:https://www.cnblogs.com/wodehao0808/p/13638009.html
Copyright © 2011-2022 走看看