- A+
前面说过场景管理器在初始化的时候就会进入一个默认的场景,它就是这个更新场景。更新场景就如其名,功能就是就检测版本更新,下载资源文件的。首先就不先看更新场景的逻辑了,一步一步来,既然要检测版本更新,就要有版本信息的文件,要下载新资源就要检测资源的是否是新,就要对应的一个记录文件md5信息的文件。
首先,主要更新的东西就是每个整场景的资源文件和一个一个的lua脚本文件,我没有将资源一个一个的细分为AssetBundle也没有将lua脚本打包成AssetBundle,甚至也没有加密lua脚本,是因为这仅仅只是我做一些自己想法的东西,很多东西就怎么方便怎么来了,如果后面有这样的需求就再修改就好了。所以这样就很简单明了了,资源就是一个一个的场景流文件和一个一个的lua脚本,将他们分别放在一个文件夹内,这样就很好的管理起来不混乱了。
资源场景的打包,都存放都一个叫Res的文件夹中,代表的就是资源文件,下面看一下把整场景打包成AssetBundle的工具类,存放的路径统一放在项目的根目录下的Res文件夹中,因为后面生成版本文件的时候就需要用到了。除了场景资源,以后所有的更新资源都放在这里,通过子文件夹分类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using UnityEditor; public class Package { [MenuItem("Package/BuildScenes")] public static void BuildSceneStream() { UnityEngine.Object[] scenes = Selection.GetFiltered(typeof(SceneAsset), SelectionMode.Assets); foreach (UnityEngine.Object o in scenes) { string path = AssetDatabase.GetAssetPath(o); if (!path.Contains(".unity")) continue; string sceneName = o.name + ".scene"; BuildPipeline.BuildPlayer(new string[] { path }, "Res/Scenes/" + sceneName, BuildTarget.Android, BuildOptions.BuildAdditionalStreamedScenes); } } } |
好了,资源文件已经准备好了,现在还需要准备的就是lua文件了,lua'文件就比较简单了,因为它们已经规范的都放置再一个叫Src的文件夹中了
那么就可以开始生成我们的版本文件了和文件的md5信息了,这里通过使用python很方便去执行。首先就是将原来准备好的资源目录和脚本目录复制到该python脚本的路径下,先说说为什么复制,因为文件都是需要上传到服务器的,为了减少上传,可以做当前版本和未来版本对比,移除不需要更新的部分(我这里没做,嘻嘻)。然后就是遍历这两个文件夹的这些文件咯,计算md5值,保存成json文件咯。
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 |
import os import json import hashlib import subprocess import shutil assetsDir = { "searchDir" : ["Src", "Res"], "ignorDir" : ["cocos", "obj","version"] } versionConfigFile = "version_info.json" #版本信息的配置文件路径 projectManifestPath = "Version/project.json" #由此脚本生成的project.manifest文件路径 versionConfigPath = "Version/version_info.json" #由此脚本生成的project.manifest文件路径 copyRes = "../Res" #原资源路径 copySrc = "../Assets/Src" #原脚本路径 targetRes = "Res" #复制到的资源路径 targetSrc = "Src" #复制到的脚本路径 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)): 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"]) return json_data def GenerateprojectManifestPath(): searchfile = SearchFile() fileList = list(searchfile.getAllFile()) project_str = {} project_str.update(getVersionInfo()) dataDic = {} for f in fileList: dataDic[f] = {"md5" : CalcMD5(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() def CopyResToUpdate(): if os.path.exists(targetRes): shutil.rmtree(targetRes) shutil.copytree(copyRes,targetRes) def CopySrcToUpdate(): if os.path.exists(targetSrc): shutil.rmtree(targetSrc) shutil.copytree(os.path.abspath(copySrc),os.path.abspath(targetSrc),ignore=_ignore_copy_files) def _ignore_copy_files(path, content): to_ignore = [] for file_ in content: if file_.endswith('.meta'): to_ignore.append(file_) return to_ignore if __name__ == "__main__": CopyResToUpdate() CopySrcToUpdate() GenerateprojectManifestPath() shutil.copy(versionConfigFile,versionConfigPath) os.system('pause') #暂停程序 |
好了,所有东西都准备好了,可以来去看看更新场景UpdateScene.lua了。很简单明了,主要在初始化的时候,获取了进度条和显示信息的文本UI,注册事件以更新UI信息,最后驱动UpdateController.lua的运行,主要更新逻辑在那里。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
local SceneBase = require("Base.SceneBase") local UpdateScene = class("Scene.UpdateScene",SceneBase) local CtrlConfig = requireCur("CtrlConfig") function UpdateScene:Main() ControllerManager:GetInstance():Init(CtrlConfig) self.updateCtrl = ControllerManager:GetInstance():Create(CtrlConfig.UpdateController) end function UpdateScene:Dispose() ControllerManager:GetInstance():Dispose() end return UpdateScene |
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 |
local ViewBase = require("Base.ViewBase") local UpdateView = class("Update.UpdateView",ViewBase) local Events = requireCur("EventsConfig") local Image = UnityEngine.UI.Image local Text = UnityEngine.UI.Text function UpdateView:Init(data) --获取场景根物体物体 local canvas = self:GetChildByName("Canvas") local loading = self:GetChildByName("Loading",canvas) self.progresssSlider = self:GetChildByName("Mask",loading):GetComponent(typeof(Image)) self.messageText = self:GetChildByName("Text",canvas):GetComponent(typeof(Text)) self.progressText = self:GetChildByName("Text",loading):GetComponent(typeof(Text)) self.quitBtn = self:GetChildByName("QuitBtn",canvas) self.updateData = data registTrigger(self.quitBtn.gameObject,Const.Triggers.OnPointerClick,handler(self,self.Quit)) self.updateMessageHanlder = registEvent(Events.UpdateViewUpdateMessage,handler(self,self.UpdateMessage)) self.showQuitBtn = registEvent(Events.ShowQuitBtn,handler(self,self.ShowQuitBtn)) end function UpdateView:UpdateMessage() self.messageText.text = self.updateData.updateTips self.progresssSlider.fillAmount = self.updateData.updateProgress self.progressText.text = (self.updateData.updateProgress - self.updateData.updateProgress % 0.01)*100 .. "%" end function UpdateView:ShowQuitBtn() self.quitBtn.gameObject:SetActive(true) end function UpdateView:Quit() UnityEngine.Application.Quit() end function UpdateView:Dispose() removeEvent(Events.UpdateViewUpdateMessage,self.updateMessageHanlder) removeEvent(Events.ShowQuitBtn,self.showQuitBtn) end return UpdateView |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
local UpdateData = class("Update.UpdateData",DataBase) local Events = requireCur("EventsConfig") function UpdateData:ctor() --视图数据 self.updateTips = '' self.updateProgress = 0 end function UpdateData:ChangeTips(tips) self.updateTips = tips callEvent(Events.UpdateViewUpdateMessage) end function UpdateData:ChangeProgress(val) self.updateProgress = val callEvent(Events.UpdateViewUpdateMessage) end return UpdateData |
下面就来看看这个最重要的更新逻辑了,首先携程调用检测版本信息,对比通过网络请求远程额版本文件信息和本地的版本文件信息,这个版本文件和同样记录md5信息的文件在打包的时候会生成一次最新的存放在Resources中,若在更新目录中找不到它们就会去Resources中去获取。如果版本信息是大版本更新,则打开官方去下载新包或内置下载也行,如果是小版本更新,则开启检测md5文件信息。同样的,是开启一个携程,对比远程下载的md5信息文件和本地的md5信息文件,遍历md5信息,新加的和md5对应不上的,都添加到下载,通过计数器来判断下载是否完成。在下载完成后,显示一个退出的按钮,提示退出应用后再进,因为如果更新了一些管理类的话不重启是不会用到最新的,暂时没想到自动重启的办法。这里有一个下载管理器,其实也是通过携程运用WWW去下载文件的,获取到byte数组数据通过工具类保存文件,也没什么好展示的了。
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 |
local ControllerBase = require("Base.ControllerBase") local UpdateController = class("Update.UpdateController",ControllerBase) local UpdateView = requireCur("UpdateView") local UpdateData = requireCur("UpdateData") local Events = requireCur("EventsConfig") local Resources = UnityEngine.Resources local TextAsset = UnityEngine.TextAsset local cjson = require "cjson" function UpdateController:Init() self.updateData = UpdateData.New()--创建交互数据 self.view = UpdateView.New(self.updateData) --创建视图 self.remoteVersionByte = nil self.remoteProjectByte = nil self.allDownloadNum = 0 self.remainDownloadNum = 0 self.updateData:ChangeTips("正在检测版本信息") coroutine.start(self.CheckVersion,self) end function UpdateController:CheckVersion() local localVersionJson = self:LodaLocalVersion() local localVersion = cjson.decode(localVersionJson) local localVersionData = string.split(localVersion.version,'.') local www = UnityEngine.WWW(Const.DownLoadUrl .. 'Version/' .. Const.VersionFile) coroutine.www(www) --等待www响应 self.remoteVersionByte = www.bytes local remoteVersionJson = tolua.tolstring(www.bytes) local remoteVersion = cjson.decode(remoteVersionJson) local remoteVersionData = string.split(remoteVersion.version,'.') if tonumber(remoteVersionData[1]) > tonumber(localVersionData[1]) then --大版本更新 这里打开官网 UnityEngine.Application.OpenURL(Const.HomeUrl) elseif tonumber(remoteVersionData[2]) > tonumber(localVersionData[2])then --小更新 下载md5文件对比下载资源 self.updateData:ChangeTips("正在下载更新文件") coroutine.start(self.CheckProject,self) else --无需更新 self:EndUpdate() end end function UpdateController:LodaLocalVersion() local filePath = PathManager:GetInstance():GetUpdatePath() .. "/" .. Const.VersionFile local f = io.open(filePath,"r") local str = '' if f == nil then --通过Resource加载 local textAsset = Resources.Load("Version/" .. Const.VersionFileName,typeof(TextAsset)) str = textAsset.text else str = f:read('*a') f:close() end return str end function UpdateController:CheckProject() local localProjectJson = self:LoadLocalProject() local localProject = cjson.decode(localProjectJson) local localProjectData = localProject.assets local www = UnityEngine.WWW(Const.DownLoadUrl .. 'Version/' .. Const.ProjectFile) coroutine.www(www) self.remoteProjectByte = www.bytes local remoteProjectJson = tolua.tolstring(www.bytes) local remoteProject = cjson.decode(remoteProjectJson) local remoteProjectData = remoteProject.assets --开始对比 然后构建下载队列 统计等待所有下载完成调用完成下载 for k,v in pairs(remoteProjectData) do if localProjectData[k] == nil or localProjectData[k].md5 ~= v.md5 then self.allDownloadNum = self.allDownloadNum + 1 self.remainDownloadNum = self.remainDownloadNum + 1 DownloadManager:GetInstance():AddDownload(Const.DownLoadUrl .. k,PathManager:GetInstance():GetUpdatePath() .. '/' .. k,handler(self,self.OnOneDownloadFinish)) end end end function UpdateController:LoadLocalProject() local filePath = PathManager:GetInstance():GetUpdatePath() .. "/" .. Const.ProjectFile local f = io.open(filePath,"r") local str = '' if f == nil then --通过Resource加载 local textAsset = Resources.Load("Version/" .. Const.ProjectFileName,typeof(TextAsset)) str = textAsset.text else str = f:read('*a') f:close() end return str end function UpdateController:OnOneDownloadFinish() self.remainDownloadNum = self.remainDownloadNum - 1 self.updateData:ChangeProgress(1 - self.remainDownloadNum / self.allDownloadNum) if self.remainDownloadNum == 0 then --保存json文件 UtilityManager:GetInstance():SaveDownLoadData(PathManager:GetInstance():GetUpdatePath() .. "/" .. Const.VersionFile,self.remoteVersionByte) UtilityManager:GetInstance():SaveDownLoadData(PathManager:GetInstance():GetUpdatePath() .. "/" .. Const.ProjectFile,self.remoteProjectByte) self.updateData:ChangeTips("更新完成,请退出应用再进") callEvent(Events.ShowQuitBtn) end end function UpdateController:EndUpdate() MySceneManager:GetInstance():LoadScene("Hall") end function UpdateController:Dispose() self.view:Dispose() end return UpdateController |
对了,lua文件下载下来了,怎么知道在lua脚本中require的是从最新的下载目录中的呢。所以需要提供一个资源加载器,ToLua中已经有了,在new一个LuaState之前new一个LuaResLoader替代LuaFileUtils,这里重写了加载的方法,是优先读取下载目录的文件还是根据SerachPath来(base)都是可以根据不同情况自己决定的。而LuaState初始化的时候会向lua环境中添加一个LuaLoader,而这个Loader会调用LuaResLoader的ReadFile方法,所以require也是用的同一个加载方式,管好自己的加载顺序就好了。
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 |
public override byte[] ReadFile(string fileName) { #if !UNITY_EDITOR byte[] buffer = ReadDownLoadFile(fileName); if (buffer == null) { buffer = ReadResourceFile(fileName); } if (buffer == null) { buffer = base.ReadFile(fileName); } #else byte[] buffer; if (Main.Instance.useRemoteRes) { buffer = ReadDownLoadFile(fileName); if (buffer == null) { buffer = ReadResourceFile(fileName); } if (buffer == null) { buffer = base.ReadFile(fileName); } } else { buffer = base.ReadFile(fileName); if (buffer == null) { buffer = ReadResourceFile(fileName); } if (buffer == null) { buffer = ReadDownLoadFile(fileName); } } #endif return buffer; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void AddLuaLoader(IntPtr L) { LuaDLL.lua_getglobal(L, "package"); LuaDLL.lua_getfield(L, -1, "loaders"); LuaDLL.tolua_pushcfunction(L, Loader); for (int i = LuaDLL.lua_objlen(L, -2) + 1; i > 2; i--) { LuaDLL.lua_rawgeti(L, -2, i - 1); LuaDLL.lua_rawseti(L, -3, i); } LuaDLL.lua_rawseti(L, -2, 2); LuaDLL.lua_pop(L, 2); } |
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 |
static int Loader(IntPtr L) { try { string fileName = LuaDLL.lua_tostring(L, 1); fileName = fileName.Replace(".", "/"); byte[] buffer = LuaFileUtils.Instance.ReadFile(fileName); if (buffer == null) { string error = LuaFileUtils.Instance.FindFileError(fileName); LuaDLL.lua_pushstring(L, error); return 1; } if (LuaConst.openLuaDebugger) { fileName = LuaFileUtils.Instance.FindFile(fileName); } if (LuaDLL.luaL_loadbuffer(L, buffer, buffer.Length, "@"+ fileName) != 0) { string err = LuaDLL.lua_tostring(L, -1); throw new LuaException(err, LuaException.GetLastError()); } return 1; } catch (Exception e) { return LuaDLL.toluaL_exception(L, e); } } |
好了,这样更新场景就完成了,在需要更新的时候,将场景资源打包,修改版本信息,运行Update.py,生成好版本文件和md5信息文件,然后将Res和Src目录文件上传到服务器中,再将Version下的版本信息文件上传,这样更新步骤就完成了。
再这里用到的registEvent、callEvent、removeEvent和UtilityManager都可以在上一篇中看到,这里就不再重复内容了。