游戏开发高阶从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
Posted 林新发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏开发高阶从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)相关的知识,希望对你有一定的参考价值。
文章目录
零、前言
嗨,大家好,我是新发。
有同学私信我,问我能不能写一篇关于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
的热更操作,大概率会被应用市场视为违规操作而下架。
注:顺便说一下,如果你使用
IL2CPP
方式打包,则你的C#
代码会被转成C++
代码。
2、热更lua代码与资源
说到游戏的热更新,就不得不提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)),
注:
GT
就是Genrate Table
的意思,在lua
中,类其实就是table
4、Generate All菜单
上面我们只是生成了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<string> searchPaths = new List<string>();
protected Dictionary<string, AssetBundle> zipMap = new Dictionary<string, AssetBundle>();
当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
来调度的,
注:如果没有
LuaLooper
,则lua
的协程会无法正常执行。
4、GameManager:游戏管理器
框架中帮我们提供了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.lua
CtrlNames = {
Prompt = "PromptCtrl",
Message = "MessageCtrl"
}
PanelNames = {
"PromptPanel",
"MessagePanel",
}
然后所有的Ctrl
在CtrlManager
中注册,
-- CtrlManager.lua
function 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.lua
local 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目录
我的代码结构如下:
以上是关于游戏开发高阶从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)的主要内容,如果未能解决你的问题,请参考以下文章
从零到一:用Phaser.js写意地开发小游戏(Chapter 1 - 认识Phaser.js)
从零到一:用Phaser.js写意地开发小游戏(Chapter 1 - 认识Phaser.js)
游戏开发进阶篇教你在Windows平台编译tolua runtime的各个平台库(Unity | 热更新 | tolua | 交叉编译)
游戏开发进阶篇教你在Windows平台编译tolua runtime的各个平台库(Unity | 热更新 | tolua | 交叉编译)