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热更新的教程。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

今天,我就来好好讲讲,内容会比较长,建议大家收藏后慢慢看。
一、我做的热更新Demo
我花了一些时间做了一个Demo,采用的是Unity + tolua,实现完整的热更流程,包括版本管理、资源打包、资源加载、lua代码加密解密、热更包下载、断点续传等功能。
1、效果演示 效果如下,下载多个增量包:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

跳过大版本更新:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

断点续传:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

2、流程图 对应的流程图如下(图片可放大):
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

首先是版本管理器,记录当前的最新版本;
启动时显示更新界面,这里就涉及到界面资源的加载,我封装了资源管理器和界面管理器,资源管理器优先从热更目录(persistentDataPathupdate)中查找资源,如果找不到才去包内的StreamingAssets目录找资源;
接着执行热更逻辑,先去Web服务器请求更新列表,判断版本号,是整包更新还是增量更新,是否是强制更新;
根据更新列表执行下载,这里我使用独立线程下载,这样不会卡住UI主线程的进度更新显示;
下载过程支持断点续传,这样可以避免下载过程中网络断开或强杀进程后需要从头开始下载;
下载完增量包后校验MD5是否正确,如果MD5不正确则重新下载;
校验MD5正确后解压到persistentDataPathupdate目录中;
启动lua框架前,先预加载luabundlelua.bundlelua_update.bundle
最后启动lua框架,显示登录界面。
另外,我单独写一套简单的打包工具,方便打AssetBundleAPP整包和增量包,
APP整包之前会先生成一份原始的lua文件的MD5列表,打lua、配置、资源等的AssetBundle,最终才生成APP整包;
另外,打包lua时我先对lua做了加密,这样可以防止被别人直接拿到lua明文文件。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

3、工程源码 我的热更新Demo工程以上传到CODE CHINA,地址:https://codechina.csdn.net/linxinfa/UnityHotUpdateFramework
感兴趣的同学可自行下载下来学习,另外,我使用的Unity版本为2021.1.7f1c1,如果你使用的版本与我的不同,可能打开工程会报错。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

关于我这个Demo的一些介绍说明,可以跳到文章第六节,接下来,我先花一点篇幅讲讲热更新和tolua框架。
二、为什么要有热更新
关于为什么要有热更新,我简单啰嗦几句。
假设你开发了一个游戏,上架到应用市场,之后用户反馈了一个严重BUG,你紧急修复后,需要重新打包APP,重新提审应用市场,经过焦急地等待,终于过审了,接着玩家需要重新下载APP,重新安装。整个流程可想而知,无法做到快速高效,而且一旦需要重新下载和安装,用户很可能就流失了。
所以我们需要有一种可以不重新安装APP就可以修复BUG的方式,那就是热更新,我们一般也叫增量更新
事实上,热更新不仅仅应用于修复BUG,也经常用于线上的小版本迭代。实际的游戏项目开发节奏是很快的,一般分为大版本迭代小版本迭代。大版本会设计比较多的开发内容,周期长,一般在两周到一个月左右;然而在同类游戏竞品的激烈竞争下,你不得不小步快跑地迭代新内容,持续给玩家新的游戏内容,拉高留存,提升活跃度。所以在大版本周期中,就会设计一些小版本迭代,以热更的方式把内容更新到线上版本,这样既不需要重新提审APP到应用商店,又不需要玩家重新下载APP和安装,一举多得。
三、Unity如何支持热更新
热更新的内容包括代码和资源,代码有C#代码、lua代码,资源包括配置表、预设、音乐音效、动画、字体、图片、材质等等,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

1、热更C#代码 Unity默认的开发语言是C#,我们写的C#代码最终会被编译成dllUnity引擎来加载。所以可以把部分C#代码编译成一个独立的dll,上传到Web服务器,启动游戏时从服务器下载dll文件,在运行时重新加载dll,通过这种方式来达到热更新的目的,不过这种方式被视为是危险操作,因为鬼知道你重新加载的dll的代码里是不是病毒,如果你的项目上架了应用市场,使用这种dll的热更操作,大概率会被应用市场视为违规操作而下架。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

注:顺便说一下,如果你使用IL2CPP方式打包,则你的C#代码会被转成C++代码。
2、热更lua代码与资源 说到游戏的热更新,就不得不提lualua这门语言是运行时动态解释的,它没运行时就是一个普通的文本文件,我们可以把它看成是资源文件。所以lua代码热更和资源热更本质是一样的,一般都是打成AssetBundle放在Web服务器,客户端从Web服务器下载最新的AssetBundle到本地。
市面上的lua框架有很多,比如toluaxluauluaslua等等,本质都是在Unity环境里内嵌一个lua虚拟机(使用c语言实现的虚拟机),游戏运行时动态解析lua脚本并执行,所以我们就可以把一些逻辑用lua来实现,然后再通过Web服务器下载lua脚本(一般是lua源码做加密后再打成AssetBundle文件,或者是使用luaclua源码编译成字节码然后再打成AssetBundle文件),从而实现热更的目的。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

四、Unity中集成tolua框架: LuaFramewrk
1、下载tolua框架: LuaFramewrk toluaGitHub地址:https://github.com/topameng/tolua
如果有同学无法访问GitHub,也可以通过Code China的镜像源来下载,
地址:https://codechina.csdn.net/mirrors/topameng/tolua
我们可以看到它提供了两个版本的框架:LuaFramework_NGUILuaFramework_UGUI
我们下载UGUI版本的:https://codechina.csdn.net/mirrors/jarjin/LuaFramework_UGUI
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

2、打开tolua框架项目:LuaFramework_UGUI 下载下来后,我们在Unity Hub中添加它,可以看到它是使用Unity5版本做的,我使用Unity2021.1.7f1c1版本打开它,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

可想而知,肯定会有一些兼容问题的报错,不要怕,我都帮你一一解决了,Unity2021.1.7f1c1版本的LuaFramework_UGUI我已上传到CODE CHINA,如果你也是使用2021版本的Unity,可以直接使用我的版本,地址:https://codechina.csdn.net/linxinfa/LuaFramework_UGUI_2021
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

不过,为了然你了解一些细节,我还是把我的解决过程写出来吧,
我建议大家往下看我是如何解决这些报错问题的,这对于你理解LuaFramework的工作原理是有帮助的,授人以鱼不如授人以渔,你是要鱼还是渔呢?
潇洒地点击确定按钮,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

3、生成注册文件:生成Wrap类 经过几分钟的载入等待,弹出了下面这个框,点击确定,它会将Unity常用的C#类生成Wrap类并注册到lua虚拟机中,这样我们就可以在lua中使用这些c#类了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

上面点击确定按钮,等效于点击菜单Lua / Gen Lua Wrap Files,所以如果你不小心点击了取消按钮,可以在菜单这里执行,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

生成Wrap成功后,我们可以在Assets/LuaFramework/ToLua/Source/Generate目录中看到很多Wrap类,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

自问:它是怎么知道要生成哪些类的Wrap类的呢?
答案就在CustomSettings.cs脚本中,如果你打开CustomSettings.cs脚本,你可以看到很多_GT(typeof(XXXXX)),如下:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

它就是根据这里来生成对应的Wrap类的,如果你想在lua中使用你自己写的类,则需要在这里加上_GT的调用,例:
_GT(typeof(MyClass)),

注:GT就是Genrate Table的意思,在lua中,类其实就是table
4、Generate All菜单 上面我们只是生成了Wrap类,事实上,还要生成Lua DelegatesLuaBinder,你可以在菜单中看到,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

生成的Wrap类需要在LuaBinder中注册到lua虚拟机中,生成的lua委托需要在DelegateFactory中注册到lua虚拟机中,当然,这些都是自动生成的,我们只需执行菜单即可。
一般我们都是直接点击菜单Lua / Generate All
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

它会做三件事情:
1 根据CustomSettings中的customDelegateList,生成lua委托并在DelegateFactory中注册到lua虚拟机中;
2 根据CustomSettings中的customTypeList,生成Wrap类;
3 在LuaBinder中生成Wrap类的注册逻辑。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

5、解决报错问题 5.1、GetElementType()为空报错 我们点击Generate All菜单后,报了如下错:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

定位到代码处:ToLuaExport.cs295行,GetElementType()可能返回空,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我们加上判空,这是ToLuaExport.cs工具的问题,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

5.2、UnityEngine_ParticleSystemWrap报错 重新点击Generate All菜单,报了新的错,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

定位到代码处:UnityEngine_ParticleSystemWrap.cs脚本,可以看到是生成的Wrap类的ParticleSystemSetParticles的异参重载函数的生成有问题,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

事实上,这个SetParticles这个方法我们基本不用在lua代码中使用到,所以简单粗暴把它注释掉就可以了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

同理,解决掉UnityEngine_ParticleSystemWrap类的同类报错。
5.3、特定的Wrap移动到BaseType中 问题来了,因为Wrap是工具生成的,上面我们这样修改Wrap类,下次重新生成的时候会被覆盖回去,就又会报错了。
解决办法是把它移到Assets / LuaFramework / ToLua / BaseType目录中,如下
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

记得在CustomSettings.cs中把对应的类的_GT调用注释掉,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我们重新点击Generate All菜单,可以看到不会帮我们重新生成UnityEngine_ParticleSystemWrap类了,不过新的问题来了,在LuaBinder中会自动帮我们注册生成的Wrap类,现在我们没有指定生成UnityEngine_ParticleSystemWrap,自然在LuaBinder中就不会帮我们生成注册的逻辑了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

没关系,BaseType中的也有一些Wrap类,它们肯定也是要注册到lua虚拟机中的,我们只需要随便找一个看看它是在哪里引用的就可以啦,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

通过引用查找,我们跳到了LuaState.cs脚本中,可以看到那些BasetType中的Wrap类是在LuaStateOpenBaseLibs函数中执行注册的,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我们只需要在这里添加上Register调用就可以了,不过需要注意命名空间,它是以BeginModuleEndModule来包裹的,多层命名空间可以嵌套,例:
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(); 之间,如下:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

5.4、LightWrap和MeshRendererWrap报错 我们看到它没有报错了,打开main场景,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

运行,闪一下,报了一个Error,如下:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我是Windows平台,所以我点击 Build Windows Resource菜单,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

报了下面新的错误:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

一个是UnityEngine_LightWrap类,一个是UnityEngine_MeshRendererWrap类,我们使用上面类似UnityEngine_ParticleSystemWrap的方法来处理即可。
我们重新执行菜单Generate AllBuild Windows Resource
可以在Assets / StreamingAssets目录中生成了很多AssetBundle文件,说明资源打包成功了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

此时我们重新运行,即可看到可以正常运行了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

五、tolua框架的工作流程
上面我们看到运行后出现了一个UI界面,这个UI界面是在lua代码中创建的,那么,Unity是如何加载并执行lua代码的呢?下面我来一步步讲,希望你耐心看完。
1、Main.cs:入口脚本 我们看回main场景的Hierarchy视图,有个GameManager
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

它身上挂着一个Main脚本,明显,这就是入口脚本,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

2、StartUp:启动游戏框架 我们打开Main.cs脚本,如下,那句StartUp就是最关键的调用,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

里面会发送一个START_UP消息,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

触发StartUpCommand类执行Execute,我们可以看到在这里添加了很多管理器,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

这些管理器会挂到GameManager物体上,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

当然,我们可以根据自己的需求添加新的管理器,也可以把不需要的管理器删掉,特别是你是项目中途继承tolua框架的话,很多管理器可能你本身项目中就已经有了,比如界面管理器、资源管理器、声音管理器、网络管理器、线程管理器等等,如果你想成为一位架构师,我建议你自己尝试去写这些管理器。
上面那些管理器中,最核心最关键的就是LuaManager,我这里要重点讲一下LuaManager
3、LuaManager:Lua管理器 LuaManager是整个tolua框架的核心,它的三个核心成员如下:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

// 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中,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

LuaState中封装了很多对LuaDLL的调度,比如调用某个lua的方法,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

LuaState又是由LuaManager来调度的,所以调度关系为LuaManager -> LuaState -> LuaDLL
启动框架时,主要是调度LuaState做了下面这些事情:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

3.2、LuaLoader:lua文件加载器 LuaLoader是文件加载器,它继承LuaFileUtils,主要提供lua文件的读取、查找功能。
核心成员变量:
public bool beZip = false; protected List searchPaths = new List(); protected Dictionary zipMap = new Dictionary();

beZipfalse时,在searchPaths中查找读取lua文件;否则从外部设置过来bundel文件中读取lua文件,我们可以重写ReadFile方法,根据自己的设计去加载lua文件,比如你对lua文件做了加密,则需要在加载这里先做解密。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

3.3、LuaLooper:lua生命周期控制 在Unity中,MonoBehaviour是有生命周期的,
可以参见Unity官方文档的说明:https://docs.unity3d.com/Manual/ExecutionOrder.html
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我们的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来调度的,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

注:如果没有LuaLooper,则lua的协程会无法正常执行。
4、GameManager:游戏管理器 框架中帮我们提供了GameManager:游戏管理器,这个我们可以自己写一个,不是用框架中的GameManager,不过我这里讲一下框架中的GameManager做了什么事情。
4.1、释放资源 Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

GameManager启动时,会先检测资源路径(Util.DataPath)中是否有lua文件,如果没有,则将StreamingAssets目录中的files.txt文件拷贝到资源路径(Util.DataPath)中,其中files.txt记录了StreamingAssets目录中所有lua文件和资源文件的md5
遍历files.txt文件,把StreamingAssets目录中的lua文件和资源文件拷贝到资源路径(Util.DataPath)中,这个过程叫做释放资源
4.2、更新资源 Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

根据AppConst.UpdateMode决定要不要执行更新资源。
如果需要更新,则访问Web服务器地址AppConst.WebUrl,下载最新的files.txt
然后遍历最新的files.txt,检查本地文件是否缺少或者MD5是否不相等,然后去Web服务器下载lua代码或资源,下载使用了线程管理器启动独立线程进行下载。
4.3、执行lua代码 更新完lua代码和资源后会调用GameManagerOnInitialize,到这里就可以启动lua虚拟机执行lua代码了。
启动lua虚拟机:
LuaManager.InitStart();

执行lua代码:
-- 加载Game.lua脚本 LuaManager.DoFile("Logic/Game"); -- 执行lua的Game.OnInitOK方法 Util.CallMethod("Game", "OnInitOK");

我们在场景中看到的界面,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

就是在Game.OnInitOK里面创建出来的,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

4.4、lua业务代码的结构 Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

lua业务代码的结构是这样的,以Demo中的界面为了例,Prompt是提示界面,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

对应一个PromptCtrl.lua脚本(界面交互逻辑,类似AndroidActivity脚本)和PromptPanel.lua脚本(界面UI对象绑定,类似于Androidlayout布局文件)。CtrlManager.lua就是管理和调度Ctrl脚本的,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

先在define.lua中定义CtrlPanel的名字,
-- define.luaCtrlNames = { Prompt = "PromptCtrl", Message = "MessageCtrl" }PanelNames = { "PromptPanel", "MessagePanel", }

然后所有的CtrlCtrlManager中注册,
-- 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#PanelManagerCreatePanel方法,
-- PromptCtrl.lua function PromptCtrl.Awake() logWarn("PromptCtrl.Awake--->>"); panelMgr:CreatePanel('Prompt', this.OnCreate); end

C#PanelManagerCreatePanel方法去加载界面预设,并挂上LuaBehaviour脚本,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

这个LuaBehaviour脚本,主要是管理Panel的生命周期,调用luaPanelAwake,获取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

界面创建后会回调CtrlOnCreate(),在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服务器我是使用小皮客户端,直接启动一个ApacheWeb服务器。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

实际项目会使用阿里云、腾讯云这些云服作为Web服务器。
增量包放在Web服务器跟目录中,Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

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目录 我的代码结构如下:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

3、资源目录结构:RawAssets目录、GameRes目录 生肉资源放在RawAssets目录中,比如动画、字体、图片等,这些资源会被预设依赖,预设是熟肉资源,相对的,这些就是生肉资源。
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

熟肉资源放在GameRes目录中,其中BaseRes放热更之前就要使用的资源,其他目录的资源都是热更后才加载的,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

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.bundleAssetBundle文件中。
我封装了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来统一调用。
画个图:
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

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;

要支持断点续传,需要判断HttpWebResponseStatusCode是否为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(); }

写文件的逻辑就是从HttpWebResponseStream流中读取数据然后写到本地的文件中,
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
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

压缩文件:
// 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
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

解密接口:
public static byte[] Encrypt(byte[] toEncryptArray)

解密接口:
public static byte[] Decrypt(byte[] toDecryptArray)

我们可以使用AssetStudio对打出来的luaAssetBundle进行逆向,可以看到逆向出来的是乱码,说明我们的加密生效了,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

注:AssetStudio下载地址:https://codechina.csdn.net/mirrors/perfare/assetstudio
11、打整包 点击菜单Build / 打包APP
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

设置你要打的整包的版本号,点击Save,然后点击Build APP即可,生成的APP会放在Assets同级目录的Bin目录中,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

Windows平台为例,如下
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

会自动生成一个LuaFrameworkFiles_版本号.json文件,里面记录了打包时的LuaFramework的文件的md5,方便后续打增量包是进行MD5对比。
12、打热更包 点击菜单Build / 打热更包
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

设置增量包的版本号,设置要读取的LuaFrameworkFiles_xxxx.json文件的版本号,根据需要添加要打进增量包的资源文件,最后点击打热更包按钮即可,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

生成的热更包会存放在Bin目录中,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我们只需将其拷贝到Web服务器,
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

并配置到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" } ] } ]

md5size可以使用Hash.exe查看,它非常的小巧,只有28KB
Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
文章图片

我也把他放在CODE CHINA上了,下载地址:
https://codechina.csdn.net/linxinfa/UnityHotUpdateFramework/-/raw/master/HexTool/Hash.exe
直接把文件拖到窗口中即可查看sizemd5了,注意它生成的是大写的md5,我们配置的时候转成小写(因为我程序里计算md5用的是小写,或者改下加个ToLower调用,这个自由发挥~)
七、完毕
【Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)】好了,就先写这么多吧~
希望可以帮助到对热更新有困惑的同学。
我是林新发:https://blog.csdn.net/linxinfa
原创不易,若转载请注明出处,感谢大家~
喜欢我的可以点赞、关注、收藏,如果有什么技术上的疑问,欢迎留言或私信~

    推荐阅读