Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章目录
-
-
- 零、前言
- 一、我做的热更新Demo
-
- 1、效果演示
- 2、流程图
- 3、工程源码
- 二、为什么要有热更新
- 三、Unity如何支持热更新
-
- 1、热更C#代码
- 2、热更lua代码与资源
- 四、Unity中集成tolua框架: LuaFramewrk
-
- 1、下载tolua框架: LuaFramewrk
- 2、打开tolua框架项目:LuaFramework_UGUI
- 3、生成注册文件:生成Wrap类
- 4、Generate All菜单
- 5、解决报错问题
-
- 5.1、GetElementType()为空报错
- 5.2、UnityEngine_ParticleSystemWrap报错
- 5.3、特定的Wrap移动到BaseType中
- 5.4、LightWrap和MeshRendererWrap报错
- 五、tolua框架的工作流程
-
- 1、Main.cs:入口脚本
- 2、StartUp:启动游戏框架
- 3、LuaManager:Lua管理器
-
- 3.1、LuaState:lua虚拟机
- 3.2、LuaLoader:lua文件加载器
- 3.3、LuaLooper:lua生命周期控制
- 4、GameManager:游戏管理器
-
- 4.1、释放资源
- 4.2、更新资源
- 4.3、执行lua代码
- 4.4、lua业务代码的结构
- 六、我的热更Demo的一些介绍说明
-
- 1、Web服务器
- 2、代码结构:Scripts目录
- 3、资源目录结构:RawAssets目录、GameRes目录
- 4、资源配置:resources.bytes、ResourcesCfg.cs
- 5、资源管理器:ResourceMgr.cs
- 6、界面管理器:PanelMgr.cs、BasePanel.cs
- 7、热更新逻辑:HotUpdater.cs
- 8、下载器:Downloader.cs
- 9、文件解压和压缩
- 10、AES对称加密解密
- 11、打整包
- 12、打热更包
- 七、完毕
-
零、前言
嗨,大家好,我是新发。
有同学私信我,问我能不能写一篇关于
ToLua
热更新的教程。文章图片
今天,我就来好好讲讲,内容会比较长,建议大家收藏后慢慢看。
一、我做的热更新Demo
我花了一些时间做了一个
Demo
,采用的是Unity + tolua
,实现完整的热更流程,包括版本管理、资源打包、资源加载、lua
代码加密解密、热更包下载、断点续传等功能。1、效果演示 效果如下,下载多个增量包:
文章图片
跳过大版本更新:
文章图片
断点续传:
文章图片
2、流程图 对应的流程图如下(图片可放大):
文章图片
首先是版本管理器,记录当前的最新版本;
启动时显示更新界面,这里就涉及到界面资源的加载,我封装了资源管理器和界面管理器,资源管理器优先从热更目录(
persistentDataPath
的update
)中查找资源,如果找不到才去包内的StreamingAssets
目录找资源;接着执行热更逻辑,先去
Web
服务器请求更新列表,判断版本号,是整包更新还是增量更新,是否是强制更新;根据更新列表执行下载,这里我使用独立线程下载,这样不会卡住
UI
主线程的进度更新显示;下载过程支持断点续传,这样可以避免下载过程中网络断开或强杀进程后需要从头开始下载;
下载完增量包后校验
MD5
是否正确,如果MD5
不正确则重新下载;校验
MD5
正确后解压到persistentDataPath
的update
目录中;启动
lua
框架前,先预加载lua
的bundle
:lua.bundle
和lua_update.bundle
;最后启动
lua
框架,显示登录界面。另外,我单独写一套简单的打包工具,方便打
AssetBundle
、APP
整包和增量包,打
APP
整包之前会先生成一份原始的lua
文件的MD5
列表,打lua
、配置、资源等的AssetBundle
,最终才生成APP
整包;另外,打包
lua
时我先对lua
做了加密,这样可以防止被别人直接拿到lua
明文文件。文章图片
3、工程源码 我的热更新
Demo
工程以上传到CODE CHINA
,地址:https://codechina.csdn.net/linxinfa/UnityHotUpdateFramework感兴趣的同学可自行下载下来学习,另外,我使用的
Unity
版本为2021.1.7f1c1
,如果你使用的版本与我的不同,可能打开工程会报错。文章图片
关于我这个
Demo
的一些介绍说明,可以跳到文章第六节,接下来,我先花一点篇幅讲讲热更新和tolua
框架。二、为什么要有热更新
关于为什么要有热更新,我简单啰嗦几句。
假设你开发了一个游戏,上架到应用市场,之后用户反馈了一个严重
BUG
,你紧急修复后,需要重新打包APP
,重新提审应用市场,经过焦急地等待,终于过审了,接着玩家需要重新下载APP
,重新安装。整个流程可想而知,无法做到快速高效,而且一旦需要重新下载和安装,用户很可能就流失了。所以我们需要有一种可以不重新安装
APP
就可以修复BUG
的方式,那就是热更新,我们一般也叫增量更新。事实上,热更新不仅仅应用于修复
BUG
,也经常用于线上的小版本迭代。实际的游戏项目开发节奏是很快的,一般分为大版本迭代和小版本迭代。大版本会设计比较多的开发内容,周期长,一般在两周到一个月左右;然而在同类游戏竞品的激烈竞争下,你不得不小步快跑地迭代新内容,持续给玩家新的游戏内容,拉高留存,提升活跃度。所以在大版本周期中,就会设计一些小版本迭代,以热更的方式把内容更新到线上版本,这样既不需要重新提审APP
到应用商店,又不需要玩家重新下载APP
和安装,一举多得。三、Unity如何支持热更新
热更新的内容包括代码和资源,代码有
C#
代码、lua
代码,资源包括配置表、预设、音乐音效、动画、字体、图片、材质等等,文章图片
1、热更C#代码
Unity
默认的开发语言是C#
,我们写的C#
代码最终会被编译成dll
由Unity
引擎来加载。所以可以把部分C#
代码编译成一个独立的dll
,上传到Web
服务器,启动游戏时从服务器下载dll
文件,在运行时重新加载dll
,通过这种方式来达到热更新的目的,不过这种方式被视为是危险操作,因为鬼知道你重新加载的dll
的代码里是不是病毒,如果你的项目上架了应用市场,使用这种dll
的热更操作,大概率会被应用市场视为违规操作而下架。文章图片
注:顺便说一下,如果你使用2、热更lua代码与资源 说到游戏的热更新,就不得不提IL2CPP
方式打包,则你的C#
代码会被转成C++
代码。
lua
,lua
这门语言是运行时动态解释的,它没运行时就是一个普通的文本文件,我们可以把它看成是资源文件。所以lua
代码热更和资源热更本质是一样的,一般都是打成AssetBundle
放在Web
服务器,客户端从Web
服务器下载最新的AssetBundle
到本地。市面上的
lua
框架有很多,比如tolua
、xlua
、ulua
、slua
等等,本质都是在Unity
环境里内嵌一个lua
虚拟机(使用c
语言实现的虚拟机),游戏运行时动态解析lua
脚本并执行,所以我们就可以把一些逻辑用lua
来实现,然后再通过Web
服务器下载lua
脚本(一般是lua
源码做加密后再打成AssetBundle
文件,或者是使用luac
将lua
源码编译成字节码然后再打成AssetBundle
文件),从而实现热更的目的。文章图片
四、Unity中集成tolua框架: LuaFramewrk
1、下载tolua框架: LuaFramewrk
tolua
的GitHub
地址:https://github.com/topameng/tolua如果有同学无法访问
GitHub
,也可以通过Code China
的镜像源来下载,地址:https://codechina.csdn.net/mirrors/topameng/tolua
我们可以看到它提供了两个版本的框架:
LuaFramework_NGUI
和LuaFramework_UGUI
。我们下载
UGUI
版本的:https://codechina.csdn.net/mirrors/jarjin/LuaFramework_UGUI文章图片
2、打开tolua框架项目:LuaFramework_UGUI 下载下来后,我们在
Unity Hub
中添加它,可以看到它是使用Unity5
版本做的,我使用Unity2021.1.7f1c1
版本打开它,文章图片
可想而知,肯定会有一些兼容问题的报错,不要怕,我都帮你一一解决了,
Unity2021.1.7f1c1
版本的LuaFramework_UGUI
我已上传到CODE CHINA
,如果你也是使用2021
版本的Unity
,可以直接使用我的版本,地址:https://codechina.csdn.net/linxinfa/LuaFramework_UGUI_2021文章图片
不过,为了然你了解一些细节,我还是把我的解决过程写出来吧,
我建议大家往下看我是如何解决这些报错问题的,这对于你理解LuaFramework的工作原理是有帮助的,授人以鱼不如授人以渔,你是要鱼还是渔呢?
潇洒地点击
确定
按钮,文章图片
3、生成注册文件:生成Wrap类 经过几分钟的载入等待,弹出了下面这个框,点击
确定
,它会将Unity
常用的C#
类生成Wrap
类并注册到lua
虚拟机中,这样我们就可以在lua
中使用这些c#
类了,文章图片
上面点击
确定
按钮,等效于点击菜单Lua / Gen Lua Wrap Files
,所以如果你不小心点击了取消
按钮,可以在菜单这里执行,文章图片
生成
Wrap
成功后,我们可以在Assets/LuaFramework/ToLua/Source/Generate
目录中看到很多Wrap
类,文章图片
自问:它是怎么知道要生成哪些类的
Wrap
类的呢?答案就在
CustomSettings.cs
脚本中,如果你打开CustomSettings.cs
脚本,你可以看到很多_GT(typeof(XXXXX))
,如下:文章图片
它就是根据这里来生成对应的
Wrap
类的,如果你想在lua
中使用你自己写的类,则需要在这里加上_GT
的调用,例:_GT(typeof(MyClass)),
注:4、Generate All菜单 上面我们只是生成了GT
就是Genrate Table
的意思,在lua
中,类其实就是table
Wrap
类,事实上,还要生成Lua Delegates
和LuaBinder
,你可以在菜单中看到,文章图片
生成的
Wrap
类需要在LuaBinder
中注册到lua
虚拟机中,生成的lua
委托需要在DelegateFactory
中注册到lua
虚拟机中,当然,这些都是自动生成的,我们只需执行菜单即可。一般我们都是直接点击菜单
Lua / Generate All
,文章图片
它会做三件事情:
1 根据
CustomSettings
中的customDelegateList
,生成lua
委托并在DelegateFactory
中注册到lua
虚拟机中;2 根据
CustomSettings
中的customTypeList
,生成Wrap
类;3 在
LuaBinder
中生成Wrap
类的注册逻辑。文章图片
5、解决报错问题 5.1、GetElementType()为空报错 我们点击
Generate All
菜单后,报了如下错:文章图片
定位到代码处:
ToLuaExport.cs
第295
行,GetElementType()
可能返回空,文章图片
我们加上判空,这是
ToLuaExport.cs
工具的问题,文章图片
5.2、UnityEngine_ParticleSystemWrap报错 重新点击
Generate All
菜单,报了新的错,文章图片
定位到代码处:
UnityEngine_ParticleSystemWrap.cs
脚本,可以看到是生成的Wrap
类的ParticleSystem
的SetParticles
的异参重载函数的生成有问题,文章图片
事实上,这个
SetParticles
这个方法我们基本不用在lua
代码中使用到,所以简单粗暴把它注释掉就可以了,文章图片
同理,解决掉
UnityEngine_ParticleSystemWrap
类的同类报错。5.3、特定的Wrap移动到BaseType中 问题来了,因为
Wrap
是工具生成的,上面我们这样修改Wrap
类,下次重新生成的时候会被覆盖回去,就又会报错了。解决办法是把它移到
Assets / LuaFramework / ToLua / BaseType
目录中,如下文章图片
记得在
CustomSettings.cs
中把对应的类的_GT
调用注释掉,文章图片
我们重新点击
Generate All
菜单,可以看到不会帮我们重新生成UnityEngine_ParticleSystemWrap
类了,不过新的问题来了,在LuaBinder
中会自动帮我们注册生成的Wrap
类,现在我们没有指定生成UnityEngine_ParticleSystemWrap
,自然在LuaBinder
中就不会帮我们生成注册的逻辑了,文章图片
没关系,
BaseType
中的也有一些Wrap
类,它们肯定也是要注册到lua
虚拟机中的,我们只需要随便找一个看看它是在哪里引用的就可以啦,文章图片
通过引用查找,我们跳到了
LuaState.cs
脚本中,可以看到那些BasetType
中的Wrap
类是在LuaState
的OpenBaseLibs
函数中执行注册的,文章图片
我们只需要在这里添加上
Register
调用就可以了,不过需要注意命名空间,它是以BeginModule
和EndModule
来包裹的,多层命名空间可以嵌套,例:BeginModule("System");
BeginModule("Generic");
System_Collections_Generic_ListWrap.Register(this);
System_Collections_Generic_DictionaryWrap.Register(this);
System_Collections_Generic_KeyValuePairWrap.Register(this);
EndModule();
//Generic
EndModule();
//end System
我们的
ParticleSystem
是在UnityEngine
命名空间下的,所以放在BeginModule("UnityEngine");
和EndModule();
之间,如下:文章图片
5.4、LightWrap和MeshRendererWrap报错 我们看到它没有报错了,打开
main
场景,文章图片
运行,闪一下,报了一个
Error
,如下:文章图片
我是
Windows
平台,所以我点击 Build Windows Resource
菜单,文章图片
报了下面新的错误:
文章图片
一个是
UnityEngine_LightWrap
类,一个是UnityEngine_MeshRendererWrap
类,我们使用上面类似UnityEngine_ParticleSystemWrap
的方法来处理即可。我们重新执行菜单
Generate All
和 Build Windows Resource
,可以在
Assets / StreamingAssets
目录中生成了很多AssetBundle
文件,说明资源打包成功了,文章图片
此时我们重新运行,即可看到可以正常运行了,
文章图片
五、tolua框架的工作流程
上面我们看到运行后出现了一个
UI
界面,这个UI
界面是在lua
代码中创建的,那么,Unity
是如何加载并执行lua
代码的呢?下面我来一步步讲,希望你耐心看完。1、Main.cs:入口脚本 我们看回
main
场景的Hierarchy
视图,有个GameManager
,文章图片
它身上挂着一个
Main
脚本,明显,这就是入口脚本,文章图片
2、StartUp:启动游戏框架 我们打开
Main.cs
脚本,如下,那句StartUp
就是最关键的调用,文章图片
里面会发送一个
START_UP
消息,文章图片
触发
StartUpCommand
类执行Execute
,我们可以看到在这里添加了很多管理器,文章图片
这些管理器会挂到
GameManager
物体上,文章图片
当然,我们可以根据自己的需求添加新的管理器,也可以把不需要的管理器删掉,特别是你是项目中途继承
tolua
框架的话,很多管理器可能你本身项目中就已经有了,比如界面管理器、资源管理器、声音管理器、网络管理器、线程管理器等等,如果你想成为一位架构师,我建议你自己尝试去写这些管理器。上面那些管理器中,最核心最关键的就是
LuaManager
,我这里要重点讲一下LuaManager
。3、LuaManager:Lua管理器
LuaManager
是整个tolua
框架的核心,它的三个核心成员如下:文章图片
// lua虚拟机
private LuaState lua;
// lua文件加载器
private LuaLoader loader;
// lua生命周期控制
private LuaLooper loop;
下面我挨个讲解他们各自做的事情。
3.1、LuaState:lua虚拟机 我们的
lua
代码需要经过lua
解释器进行解释才能执行,lua
解释器是使用c
语言写的,它在各个平台下有对应的库文件,我之前写过一篇文章:《【游戏开发进阶篇】教你在Windows平台编译tolua runtime的各个平台库(Unity | 热更新 | tolua | 交叉编译)》,里面我详细讲解了各个平台的tolua
库文件的编译,感兴趣的同学可以去看看。库函数的声明在
LuaDLL.cs
中,文章图片
LuaState
中封装了很多对LuaDLL
的调度,比如调用某个lua
的方法,文章图片
LuaState
又是由LuaManager
来调度的,所以调度关系为LuaManager -> LuaState -> LuaDLL
,启动框架时,主要是调度
LuaState
做了下面这些事情:文章图片
3.2、LuaLoader:lua文件加载器
LuaLoader
是文件加载器,它继承LuaFileUtils
,主要提供lua
文件的读取、查找功能。核心成员变量:
public bool beZip = false;
protected List searchPaths = new List();
protected Dictionary zipMap = new Dictionary();
当
beZip
为false
时,在searchPaths
中查找读取lua文
件;否则从外部设置过来bundel
文件中读取lua
文件,我们可以重写ReadFile
方法,根据自己的设计去加载lua
文件,比如你对lua
文件做了加密,则需要在加载这里先做解密。文章图片
3.3、LuaLooper:lua生命周期控制 在
Unity
中,MonoBehaviour
是有生命周期的,可以参见Unity官方文档的说明:https://docs.unity3d.com/Manual/ExecutionOrder.html
文章图片
我们的
tolua
为了实现类似的生命周期的功能,封装了一些API
,// LuaDLL.cs[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_update(IntPtr L, float deltaTime, float unscaledDelta);
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_lateupdate(IntPtr L);
[DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)]
public static extern int tolua_fixedupdate(IntPtr L, float fixedTime);
这些
API
就是由LuaLooper
来调度的,文章图片
注:如果没有4、GameManager:游戏管理器 框架中帮我们提供了LuaLooper
,则lua
的协程会无法正常执行。
GameManager
:游戏管理器,这个我们可以自己写一个,不是用框架中的GameManager
,不过我这里讲一下框架中的GameManager
做了什么事情。4.1、释放资源
文章图片
GameManager
启动时,会先检测资源路径(Util.DataPath
)中是否有lua
文件,如果没有,则将StreamingAssets
目录中的files.txt
文件拷贝到资源路径(Util.DataPath
)中,其中files.txt
记录了StreamingAssets
目录中所有lua
文件和资源文件的md5
。遍历
files.txt
文件,把StreamingAssets
目录中的lua
文件和资源文件拷贝到资源路径(Util.DataPath
)中,这个过程叫做释放资源。4.2、更新资源
文章图片
根据
AppConst.UpdateMode
决定要不要执行更新资源。如果需要更新,则访问
Web
服务器地址AppConst.WebUrl
,下载最新的files.txt
。然后遍历最新的
files.txt
,检查本地文件是否缺少或者MD5
是否不相等,然后去Web
服务器下载lua
代码或资源,下载使用了线程管理器启动独立线程进行下载。4.3、执行lua代码 更新完
lua
代码和资源后会调用GameManager
的OnInitialize
,到这里就可以启动lua
虚拟机执行lua
代码了。启动
lua
虚拟机:LuaManager.InitStart();
执行
lua
代码:-- 加载Game.lua脚本
LuaManager.DoFile("Logic/Game");
-- 执行lua的Game.OnInitOK方法
Util.CallMethod("Game", "OnInitOK");
我们在场景中看到的界面,
文章图片
就是在
Game.OnInitOK
里面创建出来的,文章图片
4.4、lua业务代码的结构
文章图片
lua
业务代码的结构是这样的,以Demo
中的界面为了例,Prompt
是提示界面,文章图片
对应一个
PromptCtrl.lua
脚本(界面交互逻辑,类似Android
的Activity
脚本)和PromptPanel.lua
脚本(界面UI
对象绑定,类似于Android
的layout
布局文件)。CtrlManager.lua
就是管理和调度Ctrl
脚本的,文章图片
先在
define.lua
中定义Ctrl
和Panel
的名字,-- define.luaCtrlNames = {
Prompt = "PromptCtrl",
Message = "MessageCtrl"
}PanelNames = {
"PromptPanel",
"MessagePanel",
}
然后所有的
Ctrl
在CtrlManager
中注册,-- CtrlManager.luafunction CtrlManager.Init()
logWarn("CtrlManager.Init----->>>");
ctrlList[CtrlNames.Prompt] = PromptCtrl.New();
ctrlList[CtrlNames.Message] = MessageCtrl.New();
return this;
end
通过
CtrlManager
获取对应的Ctrl
对象,调用Awake()
方法,-- CtrlManager.lua
local ctrl = CtrlManager.GetCtrl(CtrlNames.Prompt);
if ctrl ~= nil then
ctrl:Awake();
end
Ctrl
中,Awake()
方法中调用C#
的PanelManager
的CreatePanel
方法,-- PromptCtrl.lua
function PromptCtrl.Awake()
logWarn("PromptCtrl.Awake--->>");
panelMgr:CreatePanel('Prompt', this.OnCreate);
end
C#
的PanelManager
的CreatePanel
方法去加载界面预设,并挂上LuaBehaviour
脚本,文章图片
这个
LuaBehaviour
脚本,主要是管理Panel
的生命周期,调用lua
中Panel
的Awake
,获取UI
元素对象,-- PromptPanel.lualocal transform;
local gameObject;
PromptPanel = {};
local this = PromptPanel;
--启动事件--
function PromptPanel.Awake(obj)
gameObject = obj;
transform = obj.transform;
this.InitPanel();
logWarn("Awake lua--->>"..gameObject.name);
end--初始化面板--
function PromptPanel.InitPanel()
this.btnOpen = transform:Find("Open").gameObject;
this.gridParent = transform:Find('ScrollView/Grid');
end--单击事件--
function PromptPanel.OnDestroy()
logWarn("OnDestroy---->>>");
end
界面创建后会回调
Ctrl
的OnCreate()
,在Ctrl
中对UI
元素对象添加一些事件和控制,-- PromptCtrl.lua
--启动事件--
function PromptCtrl.OnCreate(obj)
gameObject = obj;
transform = obj.transform;
panel = transform:GetComponent('UIPanel');
prompt = transform:GetComponent('LuaBehaviour');
logWarn("Start lua--->>"..gameObject.name);
prompt:AddClick(PromptPanel.btnOpen, this.OnClick);
resMgr:LoadPrefab('prompt', { 'PromptItem' }, this.InitPanel);
end
六、我的热更Demo的一些介绍说明
1、Web服务器
Web
服务器我是使用小皮客户端,直接启动一个Apache
的Web
服务器。文章图片
实际项目会使用阿里云、腾讯云这些云服作为
Web
服务器。增量包放在
Web
服务器跟目录中, 文章图片
update_list.json
是更新列表文件,里面记录每个增量包的版本号、md5
、大小和url
,例:[
{
"appVersion": "1.0.0.0",
"appUrl": "https://blog.csdn.net/linxinfa",
"updateList":
[
{
"resVersion": "1.0.0.2",
"md5": "206933991b0fd0275695e302b9fa0839",
"size": 916897,
"url": "http://localhost:7890/res_1.0.0.2.zip"
},
{
"resVersion": "1.0.0.1",
"md5": "6d71d1648247546b43197d1ddd832ad6",
"size": 4737,
"url": "http://localhost:7890/script_1.0.0.1.zip"
}
]
}
]
2、代码结构:Scripts目录 我的代码结构如下:
文章图片
3、资源目录结构:RawAssets目录、GameRes目录 生肉资源放在
RawAssets
目录中,比如动画、字体、图片等,这些资源会被预设依赖,预设是熟肉资源,相对的,这些就是生肉资源。文章图片
熟肉资源放在
GameRes
目录中,其中BaseRes
放热更之前就要使用的资源,其他目录的资源都是热更后才加载的,文章图片
4、资源配置:resources.bytes、ResourcesCfg.cs 我把资源路径配置在
resources.bytes
中,如下:[
{ "id":1, "editor_path":"UIPrefabs/LoginPanel.prefab", "desc":"登录界面" },
{ "id":2, "editor_path":"UIPrefabs/PlazaPanel.prefab", "desc":"大厅界面" },
{ "id":3, "editor_path":"UIPrefabs/TipsFly.prefab", "desc":"提示语" }
]
editor_path
是相对GameRes
的路径,它的第一级目录将会作为AssetBundle
的名字,比如上面三个资源的一级目录都是UIPrefab
,所以他们会一起打在一个叫uiprefab.bundle
的AssetBundle
文件中。我封装了
ResourcesCfg
脚本来读取resources.bytes
,你可以通过GetResCfg
方法来获取配置,// ResourcesCfg.cspublic ResourcesCfgItem GetResCfg(int resId)
例:
var resCfg = ResourcesCfg.instance.GetResCfg(1);
5、资源管理器:ResourceMgr.cs 配置了资源后,可以通过资源管理器来加载资源,我封装了两个接口:
// ResourceMgr.cspublic T LoadAsset(int resId) where T : UObject
public T LoadAsset(string resPath) where T : UObject
你可以通过资源
id
来加载资源,例:
var loginPanelObj = ResourceMgr.instance.LoadAsset(1);
也可以通过相对路径来加载资源,
例:
var loginPanelObj = ResourceMgr.instance.LoadAsset("UIPrefabs/LoginPanel.prefab");
不过如果要显示界面,建议使用
PanelMgr
来调度和统一管理。6、界面管理器:PanelMgr.cs、BasePanel.cs 为了方便管理界面,我封装了界面基类
BasePanel
,由它来调度界面的生命周期,它有一个panelName
成员,初始化时会去查找与panelName
同名的lua
脚本,调度生命周期相关的函数,界面的创建和销毁由PanelMgr
来统一调用。画个图:
文章图片
7、热更新逻辑:HotUpdater.cs 热更新逻辑我封装在
HotUpdater
中,它做的事情如下:1 请求更新列表;
2 根据版本号计算真正需要下载的文件,最终决定是否需要更新,是强制更新还是可选更新;
3 执行下载,调度
Downloader
类完成下载任务;4 在
Update
中监听下载事件,并根据事件调用界面更新的委托函数,实现界面状态更新;5 下载完成后执行
MD5
校验,如果校验不通过,重新执行下载;6
MD5
校验通过后,执行文件解压,解压到persistentDataPath/update
目录中;7 解压完毕后删除
zip
文件;8 下载下一个增量包,直到全部下载完毕;
9 下载完毕后,回调
actionAllDownloadDone
委托函数。8、下载器:Downloader.cs
Downloader
主要就是执行下载任务,使用的是HttpWebRequest
来请求Web
服务器,var httpReq = HttpWebRequest.Create(url) as HttpWebRequest;
要支持断点续传,需要判断
HttpWebResponse
的StatusCode
是否为HttpStatusCode.PartialContent
,如果是才支持断点续传,否则要重新从头下载,var response = (HttpWebResponse)httpReq.GetResponse();
if (response.StatusCode != HttpStatusCode.PartialContent)
{
// 不能断点续传,要重新下载
}
断点续传的核心就是本地文件
Seek
到文件末尾,HttpWebRequest.AddRange
到要续传的位置,如下:m_fs = new FileStream(savePath, FileMode.OpenOrCreate, FileAccess.Write);
var lastDownloadSize = fs.Length;
m_fs.Seek(lastDownloadSize, SeekOrigin.Current);
httpReq.AddRange(lastDownloadSize);
下载文件的写文件比较耗时,使用独立的线程来执写文件,
// 开启一个独立的写文件线程
if (null == m_thread)
{
m_stopThread = false;
m_thread = new Thread(WriteThread);
m_thread.Start();
}
写文件的逻辑就是从
HttpWebResponse
的Stream
流中读取数据然后写到本地的文件中,var readSize = m_ns.Read(m_buff, 0, m_buff.Length);
if (readSize > 0)
{
m_fs.Write(m_buff, 0, readSize);
curDownloadSize += readSize;
Thread.Sleep(0);
}
else
{
// 完毕
m_stopThread = true;
state = DownloadState.End;
Dispose();
}
9、文件解压和压缩 增量包我是在打包工具中执行了压缩,压成
.zip
文件,客户端热更新时下载后会执行解压。压缩和解压我使用的库是
Ionic.Zip.Unity.dll
,文章图片
压缩文件:
// using Ionic.Zip;
using (ZipFile zip = new ZipFile())
{
// 设置压缩密码
// zip.Password = "123456";
zip.AddDirectory(Application.dataPath + "/TestDir", "./TestDir");
zip.AddFile(Application.dataPath + "/Test1.txt", "./");
zip.Save(Application.dataPath + "/result.zip");
}
解压文件:
using (ZipFile zip = new ZipFile(Application.dataPath + "/result.zip"))
{
// 设置解压密码
// zip.Password = "123456";
// 直接解压所有文件
// zip.ExtractAll(Application.dataPath + "/UnZip");
foreach (var entity in zip)
{
// 挨个文件解压
entity.Extract(Application.dataPath + "/UnZip");
}
}
10、AES对称加密解密
lua
代码打包成AssetBundle
时,我先把lua
代码拷贝到一个临时目录中并做加密,然后才执行AssetBundle
打包。我使用的加密算法是
AES
,对应的脚本是AESEncrypt.cs
,文章图片
解密接口:
public static byte[] Encrypt(byte[] toEncryptArray)
解密接口:
public static byte[] Decrypt(byte[] toDecryptArray)
我们可以使用
AssetStudio
对打出来的lua
的AssetBundle
进行逆向,可以看到逆向出来的是乱码,说明我们的加密生效了,文章图片
注:11、打整包 点击菜单AssetStudio
下载地址:https://codechina.csdn.net/mirrors/perfare/assetstudio
Build / 打包APP
,文章图片
设置你要打的整包的版本号,点击
Save
,然后点击Build APP
即可,生成的APP
会放在Assets
同级目录的Bin
目录中,文章图片
以
Windows
平台为例,如下文章图片
会自动生成一个
LuaFrameworkFiles_版本号.json
文件,里面记录了打包时的LuaFramework
的文件的md5
,方便后续打增量包是进行MD5
对比。12、打热更包 点击菜单
Build / 打热更包
,文章图片
设置增量包的版本号,设置要读取的
LuaFrameworkFiles_xxxx.json
文件的版本号,根据需要添加要打进增量包的资源文件,最后点击打热更包按钮即可,文章图片
生成的热更包会存放在
Bin
目录中,文章图片
我们只需将其拷贝到
Web
服务器,文章图片
并配置到
update_list.json
即可,如下:[
{
"appVersion": "1.0.0.0",
"appUrl": "https://blog.csdn.net/linxinfa",
"updateList":
[
{
"resVersion": "1.0.0.1",
"md5": "50d550486de1a72bd0caaa560128a9ad",
"size": 908405,
"url": "http://localhost:7890/res_1.0.0.1.zip"
}
]
}
]
md5
和size
可以使用Hash.exe
查看,它非常的小巧,只有28KB
,文章图片
我也把他放在
CODE CHINA
上了,下载地址:https://codechina.csdn.net/linxinfa/UnityHotUpdateFramework/-/raw/master/HexTool/Hash.exe
直接把文件拖到窗口中即可查看
size
和md5
了,注意它生成的是大写的md5
,我们配置的时候转成小写(因为我程序里计算md5
用的是小写,或者改下加个ToLower
调用,这个自由发挥~)七、完毕
【Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)】好了,就先写这么多吧~
希望可以帮助到对热更新有困惑的同学。
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~
推荐阅读
- Unity3D|《学Unity的猫》——第十三章(Unity使用Animator控制动画播放,皮皮猫打字机游戏)
- Unity3D|《学Unity的猫》——第十六集(Unity动画使用混合树BlendTree实现动画过渡控制)
- Unity3D|【游戏开发实战】Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码
- Unity3D|(完结)Unity游戏开发——新发教你做游戏(七)(Animator控制角色动画播放)
- 大数据|大数据计算框架与平台--深入浅出分析
- 框架|Hadoop 深入浅出 ---- 入门 (1)
- Unity常见的优化性能设置
- 框架|Mybatis的一级缓存和二级缓存
- 游戏|元宇宙密室逃脱游戏攻略来啦!