其实热更新的原理比较简单,就是本地的客户端与资源服务器进行比对相关信息与文件,然后从资源服务器获取到需要更新的资源与代码逻辑,从而完成热更新,效果图如下:

本文所有开发工具

Unity3D(个人版)-2021.3.13f1

JetBrains Rider(学生版) 2022.2.4

Sublime Text

Visual Studio Code

Typora

xLua基础

其实xLua可以说是Lua热更新的升级版,无论是在功能,性能还是易用性上都有诸多的突破的,主要表现在以下几个方面(这里主要参考了官方网站:介绍 — XLua (tencent.github.io)

  • 可以运行时把C#实现(方法,操作符,属性,事件等等)替换成lua实现;
  • 出色的GC优化,自定义struct,枚举在Lua和C#间传递无C# gc alloc;
  • 编辑器下无需生成代码,开发更轻量;

引入命名空间

在一切准备之前,需要导入相关文件,点击下方的下载按钮下载后解压直接拖到Unity3d工程文件中就ok了

点击此处下载文件

然后需要用的时候还是需要引入命名空间,这个还是比较好记忆的

1
using XLua;

xLua的基本使用

在Unity3d中执行Lua,需要使用Lua解析器,也就是 LuaEnv ,那我们就可以先初始化一下 LuaEnv

1
LuaEnv env = new LuaEnv();
注意点: 一般情况下,处于减少性能的消耗和减少执行时间,最好保持它的唯一性 那我们就可以来打印一句lua语句试试,
1
2
3
LuaEnv env = new LuaEnv();
//执行Lua语言
env.DoString("print('你好世界')");

实例结果

这里回顾一个lua知识点,在lua中双引号和单引号都是可以代表字符串的,但是在c#中是不能全都用双引号,要么就用转义字符来划分,要么就是外面双引号,里面单引号来区分,这里为了节省时间,就用第二种方式了,这样一个渐渐单单的在unity中执行lua语言就ok了

那如果是读取Lua脚本时

1
2
LuaEnv env = new LuaEnv();
env.DoString("require('main')");
知识点:
  • 执行一个Lua脚本 Lua知识点 :多脚本执行 require;
  • 默认寻找脚本的路径 是在 Resources下 并且 因为在这里,估计是通过 Resources.Load去加载Lua脚本 txt bytes等等,所以Lua脚本 后缀要加一个txt;

然后我们就可以在项目的Resources中创建一个main.lua.txt(原因上面已经说了,不过可以创建一个main.lua的lua文件,但是肯定会报LuaException: [string “chunk”]:1: module ‘main’ not found;大致意思就是找不到该文件)

1
print("lua重定向")

剩下还有两个方法分别是

1
2
env.Tick();//其作用是帮助我们清除Lua中我们没有手动释放的对象 垃圾回收,一般用于帧更新中定时执行 或者 切场景时执行
env.Dispose();//销毁Lua解析器

重定向路径

上面说到xLua解析器读取lua文件默认是在项目的Resources中,而且要把lua脚本文件的格式写成xx.lua.txt,这样在实际的开发过程中是非常不方便,况且放到项目的Resources文件中打包时是定死的,而且只可作为只读文件来使用,无法进行修改,这对于热更新的理念是背道而驰的,所以就需要用到xLua提供的路径重定向方法

1
2
//重定向路径
env.AddLoader(MyCustomLoader);

这里需要记住的便是env.AddLoader可以注册多个回调,该回调参数是字符串,字符串的名称则是需要回调的lua脚本名称,xLua解析器通过对注册的回调方法进行逐层回调,回调的返回值是一个byte数组,如果为空表示该loader找不到,否则则为lua文件的内容

这里也是设置自定义的路径的回调方法,此此方法在env.AddLoader注册后会自动执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private byte[] MyCustomLoader(ref string filePath)
{
//通过函数中的逻辑 去加载 Lua文件
//传入的参数 是 require执行的lua脚本文件名
//拼接一个Lua文件所在路径
string path = Application.dataPath + "/Lua/" + filePath + ".lua";
Debug.Log(path);
//有路径 就去加载文件
//File知识点 C#提供的文件读写的类
//判断文件是否存在
if ( File.Exists(path) ) {
return File.ReadAllBytes(path);
}
else {
Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath);
}
return null;
}

这样xLua解析器最最最基本的东西就罗列出来了

封装xLua管理器

我们可以结合唐老狮的【唐老狮】Unity中Lua热更新解决方案 - 泰课在线 — 志存高远,稳如泰山 - 国内专业的在线学习平台|Unity3d培训|Unity教程|Unity教程 Unreal 虚幻 AR|移动开发|美术CG - Powered By EduSoho (taikr.com)视频封装一套lua管理器类

首先需要写一个BaseManager,之前有说到对于lua解析器最好保证其唯一性,唯一性最大的特点便是单例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BaseManager<T> where T:new()
{
private static T _instance;
public static T GetInstance() {
//判断是否存在,当不存在时则新生成
if (_instance == null) {
_instance = new T();
}
return _instance;
}
}

在unity3d中实现热更新最重要的便是生成ab包,而ab包一般打包生成的路径是assetbundle文件夹中,所以封装一套ab包管理器

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

//知识点
//字典
//协程
//AB包相关API
//委托
//lambda表达式
//单例模式基类——>观看Unity小框架视频 进行学习
public class ABMgr : SingletonAutoMono<ABMgr>
{
//主包
private AssetBundle mainAB = null;
//主包依赖获取配置文件
private AssetBundleManifest manifest = null;
//选择存储 AB包的容器
//AB包不能够重复加载 否则会报错
//字典知识 用来存储 AB包对象
private Dictionary<string, AssetBundle> abDic = new Dictionary<string, AssetBundle>();
/// <summary>
/// 获取AB包加载路径
/// </summary>
private string PathUrl {
get { return Application.persistentDataPath + "/"; }
}
/// <summary>
/// 主包名 根据平台不同 报名不同
/// </summary>
private string MainName
{
get
{
#if UNITY_IOS
return "IOS";
#elif UNITY_ANDROID
return "Android";
#else
return "PC";
#endif
}
}

/// <summary>
/// 加载主包 和 配置文件
/// 因为加载所有包是 都得判断 通过它才能得到依赖信息
/// 所以写一个方法
/// </summary>
private void LoadMainAB() {
if (mainAB == null) {
mainAB = AssetBundle.LoadFromFile(PathUrl + MainName);
manifest = mainAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
}
}

/// <summary>
/// 加载指定包的依赖包
/// </summary>
/// <param name="abName"></param>
private void LoadDependencies(string abName) {
//加载主包
LoadMainAB();
//获取依赖包
string[] strs = manifest.GetAllDependencies(abName);
for (int i = 0; i < strs.Length; i++) {
if (!abDic.ContainsKey(strs[i])) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + strs[i]);
abDic.Add(strs[i], ab);
}
}
}
/// <summary>
/// 泛型资源同步加载
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="abName"></param>
/// <param name="resName"></param>
/// <returns></returns>
public T LoadRes<T>(string abName, string resName) where T : Object {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//得到加载出来的资源
T obj = abDic[abName].LoadAsset<T>(resName);
//如果是GameObject 因为GameObject 100%都是需要实例化的
//所以我们直接实例化
if (obj is GameObject)
return Instantiate(obj);
else
return obj;
}
/// <summary>
/// Type同步加载指定资源(ab包文件名,资源名,Type)
/// </summary>
/// <param name="abName"></param>
/// <param name="resName"></param>
/// <param name="type"></param>
/// <returns></returns>
public Object LoadRes(string abName, string resName, System.Type type) {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//得到加载出来的资源
Object obj = abDic[abName].LoadAsset(resName, type);
//如果是GameObject 因为GameObject 100%都是需要实例化的
//所以我们直接实例化
if (obj is GameObject)
return Instantiate(obj);
else
return obj;
}
/// <summary>
/// 名字 同步加载指定资源(ab包文件名,资源名)
/// </summary>
/// <param name="abName">ab包文件名</param>
/// <param name="resName">资源文件名</param>
/// <returns></returns>
public Object LoadRes(string abName, string resName) {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//得到加载出来的资源
Object obj = abDic[abName].LoadAsset(resName);
//如果是GameObject 因为GameObject 100%都是需要实例化的
//所以我们直接实例化
if (obj is GameObject)
return Instantiate(obj);
else
return obj;
}
/// <summary>
/// 泛型异步加载资源(ab包文件名,资源名,事件)
/// </summary>
/// <typeparam name="T">泛型</typeparam>
/// <param name="abName">ab包文件名</param>
/// <param name="resName">资源文件名</param>
/// <param name="callBack">调用的事件</param>
public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object {
StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callBack));
}
//正儿八经的 协程函数
private IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callBack) where T : Object {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//异步加载包中资源
AssetBundleRequest abq = abDic[abName].LoadAssetAsync<T>(resName);
yield return abq;
if (abq.asset is GameObject)
callBack(Instantiate(abq.asset) as T);
else
callBack(abq.asset as T);
}
/// <summary>
/// Type异步加载资源(ab包文件名,资源名,Type,事件)
/// </summary>
/// <param name="abName">ab包文件名</param>
/// <param name="resName">资源文件名</param>
/// <param name="type">资源的类型(UI,GameObject,Int,String......)</param>
/// <param name="callBack">调用的事件</param>
public void LoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack) {
StartCoroutine(ReallyLoadResAsync(abName, resName, type, callBack));
}
private IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callBack) {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//异步加载包中资源
AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName, type);
yield return abq;
if (abq.asset is GameObject)
callBack(Instantiate(abq.asset));
else
callBack(abq.asset);
}
/// <summary>
/// 名字 异步加载 指定资源
/// </summary>
/// <param name="abName"></param>
/// <param name="resName"></param>
/// <param name="callBack"></param>
public void LoadResAsync(string abName, string resName, UnityAction<Object> callBack) {
StartCoroutine(ReallyLoadResAsync(abName, resName, callBack));
}
private IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callBack) {
//加载依赖包
LoadDependencies(abName);
//加载目标包
if (!abDic.ContainsKey(abName)) {
AssetBundle ab = AssetBundle.LoadFromFile(PathUrl + abName);
abDic.Add(abName, ab);
}
//异步加载包中资源
AssetBundleRequest abq = abDic[abName].LoadAssetAsync(resName);
yield return abq;
if (abq.asset is GameObject)
callBack(Instantiate(abq.asset));
else
callBack(abq.asset);
}
//卸载AB包的方法
public void UnLoadAB(string name){
if (abDic.ContainsKey(name)){
abDic[name].Unload(false);
abDic.Remove(name);
}
}
//清空AB包的方法
public void ClearAB(){
AssetBundle.UnloadAllAssetBundles(false);
abDic.Clear();
//卸载主包
mainAB = null;
}
}
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
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;
/// <summary>
/// Lua管理器
/// 提供 lua解析器
/// 保证解析器的唯一性
/// </summary>
public class LuaMgr : BaseManager<LuaMgr>
{
//执行Lua语言的函数
//释放垃圾
//销毁
//重定向
private LuaEnv luaEnv;
/// <summary>
/// 得到Lua中的_G
/// </summary>
public LuaTable Global {
get {
return luaEnv.Global;
}
}
/// <summary>
/// 初始化解析器
/// </summary>
public void Init() {
//已经初始化了 别初始化 直接返回
if (luaEnv != null)
return;
//初始化
luaEnv = new LuaEnv();
//加载lua脚本 重定向
luaEnv.AddLoader(MyCustomLoader);
luaEnv.AddLoader(MyCustomABLoader);
}
//自动执行
private byte[] MyCustomLoader(ref string filePath) {
//通过函数中的逻辑 去加载 Lua文件
//传入的参数 是 require执行的lua脚本文件名
//拼接一个Lua文件所在路径
string path = Application.dataPath + "/Lua/" + filePath + ".lua";
//有路径 就去加载文件
//File知识点 C#提供的文件读写的类
//判断文件是否存在
if (File.Exists(path)) {
return File.ReadAllBytes(path);
}
else {
Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath);
}
return null;
}
//Lua脚本会放在AB包
//最终我们会通过加载AB包再加载其中的Lua脚本资源 来执行它
//重定向加载AB包中的LUa脚本
private byte[] MyCustomABLoader(ref string filePath) {
//Debug.Log("进入AB包加载 重定向函数");
////从AB包中加载lua文件
////加载AB包
//string path = Application.streamingAssetsPath + "/lua";
//AssetBundle ab = AssetBundle.LoadFromFile(path);
////加载Lua文件 返回
//TextAsset tx = ab.LoadAsset<TextAsset>(filePath + ".lua");
////加载Lua文件 byte数组
//return tx.bytes;
//通过我们的AB包管理器 加载的lua脚本资源
TextAsset lua = ABMgr.GetInstance().LoadRes<TextAsset>("lua", filePath + ".lua");
if (lua != null)
return lua.bytes;
else
Debug.Log("MyCustomABLoader重定向失败,文件名为:" + filePath);
return null;
}
/// <summary>
/// 传入lua文件名 执行lua脚本
/// </summary>
/// <param name="fileName"></param>
public void DoLuaFile(string fileName) {
string str = string.Format("require('{0}')", fileName);
DoString(str);
}
/// <summary>
/// 执行Lua语言
/// </summary>
/// <param name="str"></param>
public void DoString(string str) {
if(luaEnv == null) {
Debug.Log("解析器为初始化");
return;
}
luaEnv.DoString(str);
}
/// <summary>
/// 释放lua 垃圾
/// </summary>
public void Tick() {
if (luaEnv == null) {
Debug.Log("解析器为初始化");
return;
}
luaEnv.Tick();
}
/// <summary>
/// 销毁解析器
/// </summary>
public void Dispose() {
if (luaEnv == null) {
Debug.Log("解析器为初始化");
return;
}
luaEnv.Dispose();
luaEnv = null;
}
}

基本上这样就封装好了,具体的使用就比较容易了

1
2
3
4
//初始化解析器
LuaMgr.GetInstance().Init();
//执行lua脚本
LuaMgr.GetInstance().DoLuaFile("main"); //这里会调用MyCustomLoader,然后判断文件是否存在,若存在则读取打印出所有

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2hc8e6q8fxa8w