ulua热更新小试
传说中的热更新在Unity中是怎样实现的:)
写在前面
热更新技术在游戏行业可以说是大名鼎鼎了,虽然苹果前段时间禁止了JSPatch等热更新技术,但目前来看,苹果并没有禁止游戏引擎的热更新技术。某种程度上说明了热更新在游戏中的重要性。而ulua作为一款优秀的unity3d热更新插件,完美解决了Unity游戏热更新的问题。
什么是热更新
热更新一般用于网络游戏中。其指的是在不重新下载客户端的情况下,对游戏的内容进行更新(包括资源更新或逻辑更新等)。知乎上对热更新有一个很形象的比喻:假设你的卡车开到了150KM/H,然后有个轮胎爆了。司机说,你就直接换吧,我不停车。你小心点换。热更新机制大概就是这个意思。
什么是Lua
Lua是一款轻巧的脚本语言,由标准C编写而成,代码简洁优美,几乎在所有操作系统和平台上都可以编译,运行。在目前所有脚本引擎中,Lua的速度是最快的。这一切都决定了Lua是作为嵌入式脚本的最佳选择。(嗯这段其实是百度的orz…)
Lua代码都是运行时才编译的,不运行的时候就如同一张图片、一段音频一样,都是文件;所以更新逻辑只需要更新脚本,不需要再编译,因而Lua能轻松实现“热更新”。Ulua是一款非常实用的unity插件,它能让unity支持Lua语言,而且运行效率还不错。
使用ulua进行热更新
1.安装ulua插件 及 Lua编写工具LuaStudio
- ulua下载地址:http://ulua.org/
- LuaStudio下载地址:https://pan.baidu.com/s/1hsabx0w 密码: kqvp
2.新建Unity工程,将ulua导入工程中
3.ulua中的使用流程
- 实例化LuaState对象(new LuaState()),一个LuaState对象代表一个Lua解释器
- 加载Lua代码(LuaState.DoString(string)),string为Lua代码字符串或Lua脚本文件名称(推荐使用后者)
- 调用Lua代码中的方法(GetFunction string),LuaFunction.callFunction(string)
- 注:由于Unity不支持扩展名为lua的文件,所以可将Lua脚本扩展名定为txt(纯文本文件),并用unity的TextAsset列表负责记录所有脚本文件。建议列表中给每个脚本搭配一个string类型的ID,这样凭此ID即可加载正确的lua脚本;另外在LuaState类中新增一个String类型的public成员,赋值为该ID。这样一旦某个Lua脚本在运行时报错,可根据输出的ID值判断是哪个Lua脚本有错误。
4.ulua框架在Unity中的使用(SimpleFramework_UGUI解读)
- 框架启动
- GlobalGenerator:初始化游戏环境,包括添加AppView,启动pureMVC框架,添加各种Manager
- GameManager中对资源进行更新处理
- 资源初始化过程 OnResourceInited
- 加载网络、游戏管理器的Lua脚本
- 调用GameManager.lua里的LuaScriptPanel方法(CallMethod通过LuaScriptMgr.cs的CallLuaFunction()将控制权移交给GameManager.lua)
- 创建Lua面板(Message、Prompt)
- 调用方法OnInitOK表示初始化成功
- ulua框架的执行顺序:
- 每个UI Panel对应View下的lua代码,用来获取一些需要交互的属性
- 每个UIprefab通过Controller进行控制,包括其实例化以及组件的一些行为,比如OnCreate事件
热更新案例:UI面板更新
创建开发UI界面
- 设计UI Panel
- 将UI panel做成prefab,进行打包(注意后缀一定为.assetbundle)
- 将UI panel所用到的所有UI资源进行打包(图片、字体等),最好分类打包
- 点击Game-Build XXX Resources(XXX 代表想要发布到的平台)
- 创建Global Generator(空Object上挂Global Generator脚本)
- 重写Logic文件夹中的GameManager.lua脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21require "Common/define"
require "Controller/BottomCtrl" --以下引入对各子面板的控制器
require "Controller/SettingsCtrl"
require "Controller/DialogCtrl"
GameManager = {}
function ()
return 'Bottom','Settings','Dialog'; --Prefab中除掉“Panel”后的名字
end
function GameManager.OnInitOK()
--加载网络
AppConst.SocketPort = 2012; --设置套接字端口号
AppConst.SocketAddress = "127.0.0.1"; --设置套接字IP地址,这里默认从主机下载资源
NetManager:SendConnect(); --建立连接
BottomCtrl.Awake();
SettingsCtrl.Awake();
DialogCtrl.Awake();
end - View文件夹下创建子面板的lua脚本(以BottomPanel.lua为例,其他同理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20BottomPanel = {}
local this = BottomPanel
local gameObject
local transform
function BottomPanel.Awake(obj)
--对局部变量进行赋值
gameObject = obj;
transform = gameObject.transform
this.InitPanel();--初始化面板
end
function BottomPanel.InitPanel()
--给面板中的三个Button赋值
this.buttonSettings = transform:FindChild("ButtonSetting").gameObject;
this.buttonPeople = transform:FindChild("ButtonPeople").gameObject;
this.buttonDialog = transform:FindChild("ButtonDialog").gameObject;
end - [可选]给SettingPanel下的BG添加动画:由小变大动画、隐藏动画、激活动画
- 重写SettingPanel.lua脚本获取UI中的组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18SettingsPanel = {}
local this = SettingsPanel
local transform
local gameObject
function SettingsPanel.Awake(obj)
gameObject = obj;
transform = gameObject.transform;
this.InitPanel();
end
function SettingsPanel.InitPanel()
--获取动画组件及按钮组件
this.anim = transform:FindChild("Bg"):GetComponent("Animator");
this.buttonClose = transform:FindChild("Bg/ButtonClose").gameObject;
end - 开发Controller控制层下的Lua代码,控制UI控件的产生和事件的监听(以BottomCtrl.lua为例)
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
40require "Common/define"
BottomCtrl = {}
local this = BottomCtrl
local gameObject
local transform
local lua
function BottomCtrl.New()
return this;
end
function BottomCtrl.Awake()
--调用panel的创建方法创建相应面板,注意面板命名为XXPanel
PanelManager:CreatePanel("Bottom",this.OnCreate)
end
function BottomCtrl.OnCreate(obj)
gameObject = obj;
transform = obj.transform
lua = gameObject:GetComponent("LuaBehaviour");
--注册按钮,绑定事件
lua:AddClick(BottomPanel.buttonDialog,this.OnButtonDialogClick);
lua:AddClick(BottomPanel.buttonPeople,this.OnButtonPeopleClick);
lua:AddClick(BottomPanel.buttonSettings,this.OnButtonSettingsClick);
end
function BottomCtrl.OnButtonDialogClick()
DialogCtrl.Show();
end
function BottomCtrl.OnButtonPeopleClick()
end
function BottomCtrl.OnButtonSettingsClick()
SettingsCtrl.Show();
end - 发布到手机上,启动Server
- Switch Platform - Android
- Lua-Clear Wrap Files
- Lua-Gen Wrap Files
- Game-Build Android Resources
- 修改AppConst.cs里的UpdateMode=true,DebugMode=false,WebUrl=局域网地址(有服务器的话就是服务器地址,这里假设用uLua自带服务器运行)
- 打开Server.sln-HttpServer.cs,修改host,重新生成工程
- 运行ulua文件夹下Server/Server/bin/Debug/SuperSocket.SocketService.exe(以管理员权限运行),选择r运行服务器
- 进行Lua代码的更新
- Build & run,手机连电脑,将程序发布到手机
- 更改Lua代码
- 重新Build Android Resources
- 手机重新启动该程序
- 解包完成!更新完成!
- 进行UI资源的更新
- 创建Dialog Panel,打包,新建Panel,Ctrl脚本文件
- 重新Build Android Resources
- 手机重新启动软件
注意:如果电脑防火墙没关,手机是没有权限访问电脑的,就会更新失败。。。
Unity3D中的热更新
Unity3D的热更新会涉及3个目录。
游戏资源目录:里面包含Unity3D工程中StreamingAssets文件夹下的文件。安装游戏之后,这些文件将会被一字不差地复制到目标机器上的特定文件夹里,不同平台的文件夹不同,如下所示
- Mac OS或Windows:Application.dataPath “/StreamingAssets”;
- IOS: Application.dataPath “/Raw”;
- Android:jar:file://“ Application.dataPath “!/assets/“;
数据目录:由于“游戏资源目录”在Android和IOS上是只读的,不能把网上的下载的资源放到里面,所以需要建立一个“数据目录”,该目录可读可写。第一次开启游戏后,程序将“游戏资源目录”的内容复制到“数据目录中”(这个步骤只会执行一次,下次再打开游戏就不复制了)。游戏过程中的资源加载,都是从“数据目录”中获取、解包。不同平台下,“数据目录”的地址也不同,LuaFramework的定义如下:
- Android或IOS:Application.persistentDataPath “/LuaFramework”
- Mac OS或Windows:c:/LuaFramework/
- 调试模式下:Application.dataPath “/StreamingAssets/“
注:”LuaFramework”和”StreamingAssets”由配置决定,这里取默认值
- 网络资源地址:存放游戏资源的网址,游戏开启后,程序会从网络资源地址下载一些更新的文件到数据目录。
这些目录包含着不同版本的资源文件,以及用于版本控制的files.txt。Files.txt里面存放着资源文件的名称和md5码。程序会先下载“网络资源地址”上的files.txt,然后与“数据目录”中文件的md5码做比较,更新有变化的文件。
常见问题 && 注意事项
- 运行 LuaStudio 时,请使用Administrator管理员权限!
- Lua需要统一的UTF-8编码,有时候Lua脚本无故编译出错请检查编码问题!
- 若运行到真机,记得一定要设置Const.DebugMode=false
- 【该点摘录自云风博客】更新时要保护后内存中的非代码数据。这个时候,对 local 变量的使用务必小心。因为 local 变量总会被作为 upvalue 绑定在 closure 里。我们的代码经常会依赖这些 local 变量。在更新后,许多保存数据用的 local 变量会生成新的一份。这很可能丢失重要数据。而因为这个问题回避使用 local 也是不合适的。要知道 local 和 global 变量的性能可不只差上一点半点。
我采用的方法是,把数据记录在专用的全局表下,并用 local 去引用它。初始化这些数据的时候,首先应该检查他们是否被初始化过了。这样来保证数据不被更新过程重置