xLua热更新(一)xLua基本使用

article/2025/9/28 2:34:30

一、什么是xLua

xLua为Unity、 .Net、 Mono等C#环境增加Lua脚本编程的能力,借助xLua,这些Lua代码可以方便的和C#相互调用。

xLua是用来实现Lua代码与C#代码相互调用的插件。我们可以借助这个插件来实现热更新方案。

那么为什么要选择Lua实现热更新呢?

这是因为Lua具有轻量、灵活的特点,可以在几乎任何平台上编译、运行。Unity一般使用C#代码编写游戏逻辑。在打包时,C#会先编译成IL(中间语言),存储到dll(动态链接库)中。在游戏运行时,需要通过JIT(即时编译)将IL解释为机器码。在这期间,会开辟一块内存空间,且要求这块空间可读、可写、可执行。但IOS平台是不允许获取具有可执行权限的内存空间的。因此只能进行全量更新。但Lua是使用C写的脚本语言,在运行时读入Lua代码,在解释时直接使用C代码进行解释,不需要开辟特殊的内存空间,执行解释的是C语言编写的虚拟机。(参考自这篇文章)

二、如何使用

2.1 Hello World

首先在xLua的GitHub主页下载源码,并引入到Unity中。

创建一个C#脚本,并编写如下代码。DoString()方法可以执行传入的Lua代码。

public class HelloWorld : MonoBehaviour
{private LuaEnv _lua;private void Start(){_lua = new LuaEnv();_lua.DoString("print('Hello World')");}private void OnDestroy(){_lua.Dispose();_lua = null;}
}

将脚本挂载到一个游戏物体上,运行游戏。可以在控制台看到输出结果,且输出的字符串带有“Lua:”前缀。

需要注意的是,一个LuaEnv的实例对应着一个Lua虚拟机,建议全局唯一。

2.2 加载Lua文件

DoString()方法中直接写大量的Lua代码是不现实的,我们需要载入外部的Lua文件,并将文件内容传入这个方法中执行。

首先编写一个简单的Lua脚本,并将脚本放在「Resources」目录下

a = 2  
b = 3  
print("a+b="..a+b)

然后在C#脚本中通过Resources进行载入

public class LoadLuaFile : MonoBehaviour
{private void Start(){var lua = Resources.Load<TextAsset>("AddLua");Debug.Log(lua);if (lua != null){LuaEnv luaEnv = new();luaEnv.DoString(lua.text);luaEnv.Dispose();}}
}

此时运行游戏我们会发现,控制台输出的是「Null」。这是因为Resources.Load<TextAsset>()会默认给文件名后面增加「.txt」后缀。也就是说这个方法只会读取到后缀为「.txt」的文件。

为了能读取到Lua文件,我们需要将Lua文件的后缀改为「.lua.txt」。在加载时,文件名传入「XXX.lua」,这样就能顺利读取到Lua脚本的内容了。

我们也可以使用xLua内置的loader进行加载。方法是在luaEnv.DoString()方法中直接传入require语句。require会调一个个的Loader去加载,直到遇到不返回空的Loader。如果全部返回空则会报文件找不到的异常。

LuaEnv luaEnv = new();  
luaEnv.DoString("require 'AddLua'");  
luaEnv.Dispose();

如果lua脚本没有问题,但运行时“unexpected symbol”之类的错误,可以用记事本打开lua脚本,重新保存为UTF-8编码格式。

2.3 自定义Loader

某些情况下系统内置的Loader并不能满足我们的需求。比如需要对Lua文件解密,或者Lua文件不在「Resources」目录下等。这时就需要我们自定义Loader。

要实现自定义Loader也很简单,只需要调用LuaEnv.AddLoader()方法,添加一个自定义Loader即可。该方法需要传入一个委托,委托的参数是加载文件的路径,返回值是文件内容的字节数组。

public class CustomLoader : MonoBehaviour
{private void Start(){LuaEnv luaEnv = new();luaEnv.AddLoader(MyLoader);luaEnv.DoString("require '不存在的文件'");luaEnv.Dispose();}private byte[] MyLoader(ref string filePath){string content = "print('Hello World')";return System.Text.Encoding.UTF8.GetBytes(content);}
}

上面的代码运行结果如下

要实现加载指定目录的Lua文件,只需要在自定义Loader中通过文件流读取文件即可

string path = "[指定路径]";  
return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(path));

2.4 C#访问Lua

2.4.1 访问全局变量

Lua脚本

str="Hello World"  
num=12  
isTrue=true

C#脚本

LuaEnv luaEnv = new();  
luaEnv.DoString("require 'CSharpCallLua'");  
var str = luaEnv.Global.Get<string>("str");  
var num = luaEnv.Global.Get<int>("num");  
var isTrue = luaEnv.Global.Get<bool>("isTrue");  
Debug.Log($"str:{str} num:{num} isTrue:{isTrue}");  
luaEnv.Dispose();

运行结果

2.4.2 访问全局table

映射到class或struct

Lua脚本

person = {  Name="宇智波佐助",  Age=12  
}

C#脚本

class Person  
{  public string Name;  public int Age;  
}  private void CallTableToClass()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'CSharpCallLua'");  var person = luaEnv.Global.Get<Person>("person");  Debug.Log($"name:{person.Name} age:{person.Age}");  luaEnv.Dispose();  
}

运行结果

需要注意的是,这个映射过程是值拷贝,如果class比较复杂,代价会比较大。且因为是值拷贝,无论去修改任何一边的字段值,另一边也不会同步修改。

映射到interface

Lua脚本

person = {  Name="宇智波佐助",  Age=12,  Do=function(self,a,b) -- 需要额外定义一个参数self,相当于this  print(a+b)  end  
}

C#脚本

[CSharpCallLua]
public interface IPerson // 接口必须声明为public
{  string Name { get; set; }  int Age { get; set; }  void Do(int a,int b);  
}private void CallTableToInterface()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'CSharpCallLua'");  var person = luaEnv.Global.Get<IPerson>("person");  Debug.Log($"name:{person.Name} age:{person.Age}");  person.Do(12,3);  luaEnv.Dispose();  
}

运行结果

这是引用方式的映射,也就是说在C#中更改字段值,对应的Lua表中的值也会跟着修改。另外,如果映射的方法具有参数,则在Lua代码中,需要在最前面额外定义一个接收参数,用来充当this。或者通过如下方式定义方法

function person:Do(a,b)  print(a+b)  
end

另外在运行时可能会碰到如下问题:

首先检查映射的接口上有没有加[CSharpCallLua]特性。如果已经添加该特性,且Unity版本是2018以上,那就将「File->Build Settings->Player Settings->Player->Other Settings->Configuration->Api Compatibility Level」中的选项改为「.NET Framework」。具体原因可以查看xLua官方文档中的「faq」文档

映射到集合

Lua脚本

person = {  Name="宇智波佐助",  Age=12,  Do=function(self,a,b)  print(a+b)  end,  1,2,3,  "Hello",  true  
}

C#脚本

private void CallTableToCollection()
{LuaEnv luaEnv = new();luaEnv.DoString("require 'CSharpCallLua'");var person1 = luaEnv.Global.Get<Dictionary<string,object>>("person");foreach (var e in person1){Debug.Log($"Key:{e.Key} Value:{e.Value}");}Debug.Log("------------------------------------------------");var person2 = luaEnv.Global.Get<List<object>>("person");foreach (var e in person2){Debug.Log(e);}luaEnv.Dispose();
}

运行结果

可以看到,在table中显式定义了键值的字段都可以正常映射到字典中,而没有定义键值的则会丢失;线性表则与之相反,只会映射没有定义键值的字段。

映射到LuaTable

Lua脚本

person = {  Name="宇智波佐助",  Age=12,  Do=function(self,a,b)  print(a+b)  end,  1,2,3,  "Hello",  true  
}

C#脚本

private void CallTableToLuaTable()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'CSharpCallLua'");  var person = luaEnv.Global.Get<LuaTable>("person");  Debug.Log($"{person.Get<int,int>(1)}");  Debug.Log($"{person.Get<int,int>(2)}");  Debug.Log($"{person.Get<int,int>(3)}");  Debug.Log($"{person.Get<int,string>(4)}");  Debug.Log($"{person.Get<int,bool>(5)}");  Debug.Log($"{person.Get<string,string>("Name")}");  Debug.Log($"{person.Get<string,int>("Age")}");  Debug.Log($"{person.Get<string,object>("Do")}");  luaEnv.Dispose();  
}

运行结果

LuaTable类是xLua提供的类,它可以把定义了键值和未定义键值的字段全部映射过来。但是性能上要慢很多,且没有类型检查。因而一般很少会使用这种方法。

2.4.3 访问全局函数

映射到委托

对于Lua中无参数和返回值的函数,可以使用C#中的Action接收
Lua脚本

function Add1()  print("调用了Add")  
end

C#脚本

private void CallFunctionToDelegate()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'CSharpCallLua'");  Action act = luaEnv.Global.Get<Action>("Add1");  act();  // 延时调用确保引用被释放  StartCoroutine(DisposeLuaEnv(luaEnv));  
}  IEnumerator DisposeLuaEnv(LuaEnv luaEnv)  
{  yield return new WaitForSeconds(0.1f);  luaEnv.Dispose();  
}

运行结果

如果是有返回值和参数的函数,可以自定义一个委托来接收。对于有多个返回值的函数,可以使用out参数接收。
Lua脚本

function Add2(a,b)  print("调用了Add 结果:"..a+b)  return a+b,"Hello",true  
end

C#脚本

[CSharpCallLua]  
private delegate int Add(int a, int b, out string res2, out bool res3);private void CallFunctionToDelegate()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'CSharpCallLua'");  int res1 = add(12,3,out string res2,out bool res3);  Debug.Log($"res1:{res1} res2:{res2} res3:{res3}");  // 延时调用确保引用被释放StartCoroutine(DisposeLuaEnv(luaEnv));
}IEnumerator DisposeLuaEnv(LuaEnv luaEnv)  
{  yield return new WaitForSeconds(0.1f);  luaEnv.Dispose();  
}

运行结果

映射到LuaFunction

这种方式是将函数映射到xLua提供的LuaFunction类中,写起来比较简单,但是性能要比委托的方式差。
Lua脚本

function Add2(a,b)  print("调用了Add 结果:"..a+b)  return a+b,"Hello",true  
end

C#脚本

private void CallFunctionToLuaFunction()
{LuaEnv luaEnv = new();luaEnv.DoString("require 'CSharpCallLua'");var add = luaEnv.Global.Get<LuaFunction>("Add2");var res = add.Call(12, 3);foreach (var e in res){Debug.Log(e);}luaEnv.Dispose();
}

运行结果

2.5 Lua访问C#

在Lua中访问C#脚本的成员比较简单,只需要在所有C#相关的代码都加上CS前缀即可
C#脚本

private void Start()  
{  LuaEnv luaEnv = new();  luaEnv.DoString("require 'LuaCallCSharp'");  luaEnv.Dispose();  
}

Lua脚本

-- 实例化对象  
local go = CS.UnityEngine.GameObject("LuaGameObject")  -- 访问静态属性、方法  
local deltaTime = CS.UnityEngine.Time.deltaTime  
local GameObject = CS.UnityEngine.GameObject  
local camera = GameObject.Find("Main Camera")  -- 访问成员属性、方法  
camera.name = "LuaCamera"  
camera:GetComponent("Camera").clearFlags = CS.UnityEngine.CameraClearFlags.SolidColor

再次强调在Lua中调用成员方法时,要么使用:的方式访问,要么使用.调用并在参数中额外传入对象本身的方式访问。


http://chatgpt.dhexx.cn/article/dV9f5XNM.shtml

相关文章

Bug-CTF-秋名山老司机(正则匹配)

题目: 没有啥思路&#xff0c;意外地刷新了一下页面&#xff0c;发现数值变化了 再刷新一次试试&#xff0c;出来一个提示&#xff0c;大概意思是需要提交结果&#xff0c;这里也不知道该怎么传参&#xff0c;也不晓得怎么写这个脚本&#xff0c;只能参考其他大佬的思路了 解题…

BUGKU------秋名山老司机

看到这个就直接上python吧&#xff0c;用eval计算子式 import requests from bs4 import BeautifulSoup r requests.session() s r.get(http://123.206.87.240:8002/qiumingshan/) soup BeautifulSoup(s.text, "html.parser") a soup.find(div) d {"valu…

bugku秋名山车神

不断的刷新&#xff0c;发现表达式一直在变换&#xff0c;这种必须写脚本&#xff0c;才能跟上速度。直接上代码 import re import requests srequests.session() rs.get("http://123.206.87.240:8002/qiumingshan/") searchObj re.search(r^<div>(.*)\?;<…

【BugkuCTF】Web--秋名山老司机

Description: http://123.206.87.240:8002/qiumingshan/ 是不是老司机试试就知道。 Solution: 打开网页 2秒解决问题真是稳稳的写脚本……但是不知道提交啥&#xff0c;刷新网页看看提示让用POST方式传递一个value变量&#xff0c;构造脚本 import requests import re url htt…

CTF-web-秋名山老司机

前言&#xff1a;小编也是现学现卖&#xff0c;方便自己记忆&#xff0c;写的不好的地方还请包涵&#xff0c;也欢迎各位大佬多多批评指正。 网址&#xff1a;秋名山老司机 1.打开网址&#xff0c;提示需要两秒内计算出数值&#xff0c;手工几乎不可能实现。 2.思路:利用pyt…

秋名山车神

解题思路&#xff1a;看到这种题要在两秒类算出&#xff0c;人工肯定不可能&#xff0c;直接上脚本&#xff0c;由于我不会写python 脚本&#xff0c;直接在网上找了一篇大佬的脚本 import requests #安装requests库 import re url http://114.67.246.176:16847 #改为自己题…

爬虫笔记-Bugku秋名山老司机(入门)

记一次python爬虫笔记 题目&#xff1a;bugku-秋名山老司机 题目要求&#xff1a;两秒内提交一道很长的计算题答案&#xff0c;并且式子每次刷新都会变动 如&#xff1a; 多刷新几次可见题目提示&#xff0c;需要用post传入值&#xff0c;变量名为value 创建py文件&#xf…

ctf靶场-bugku-秋名山老司机,速度要快

页面快速计算(秋名山老司机) 1.靶场网址 http://123.206.87.240:8002/qiumingshan/ 2.脚本实现 import requests import re url"http://123.206.87.240:8002/qiumingshan/" srequests.Session() #储存session rs.get(url) #用此身份执行get请求 searchobjre.sea…

[bugku]-秋名山车神详解

解题 每一次刷新都不一样 post传参value 脚本1 import requests import re url http://114.67.175.224:10053/ s requests.Session() source s.get(url) expression re.search(r(\d[\-*])(\d), source.text).group() result eval(expression) post {value: result} prin…

Bugku_Web18_秋名山车神

1.查看源码 <head> <title>下面的表达式的值是秋名山的车速</title> <meta charset"UTF-8"> </head> <p>亲请在2s内计算老司机的车速是多少</p> <div>373719747-1878154638-1233431774-1476346255*1056350133121800…

bugku 秋名山车神

get新知识&#xff1a;一些有关python爬虫的基本知识 解题部分 题目中都是这样的大数字进行计算&#xff0c;并且需要短时间内计算&#xff0c;所以这不得不使用脚本进行解题&#xff0c;脚本如下 #bugku 秋名山车神 --爬虫练习 import requests import re ​ srequests.Sess…

web——秋名山老司机(100)——Bugku

000 靶场链接 http://123.206.87.240:8002/web16/ 001 题目描述 002 解题过程 一看大数运算就觉得要用python&#xff0c;然后就只能找wp看看 多刷新几次会有提示出来 一个value post 创建步骤 先建立文本文档&#xff0c;后缀改为.py&#xff0c;然后右键使用IDE打开 代…

求秋名山老司机车速

http://120.24.86.145:8002/qiumingshan/ 两秒内算出秋名山车神的车速。感觉很吊的。 刚开始知道的大概的知道就是算出答案&#xff0c;但是怎么提交&#xff0c;后来多刷新几次发现。 给我post value。好吧&#xff0c;翻译也翻译的很蛋疼。反正就是需要value这个作为提交的参…

秋名山老司机从上车到翻车的悲痛经历,带你深刻了解什么是 Spark on Hive!| 原力计划...

作者 | Alice菌 责编 | 夕颜 出品 | CSDN博客 本篇博客将为大家分享的内容是如何实现Spark on Hive&#xff0c;即让Hive只作为存储角色&#xff0c;Spark负责sql解析优化&#xff0c;执行…话不多说&#xff0c;直接上车&#xff01; 上车前需知 Spark on hive 与 hive on spa…

秋名山老司机 (Bugku) re库和request库

尝试写的第一个python脚本……之前一直只会用工具&#xff08;不&#xff0c;有的工具也还不会用……&#xff09;可以说是很神奇了 先贴上代码&#xff1a; import requests import re urlhttp://120.24.86.145:8002/qiumingshan/ rrequests.session() requestpage r.get(u…

bagku秋名山老司机

看题目,要求两秒内计算数值,发回去,获得flag,于是写脚本实现 import requests import reurl "http://120.24.86.145:8002/qiumingshan/" s requests.Session()#必须利用会话对象 Session()&#xff0c;否则提交结果的时候&#xff0c;页面又重新生成一个新的表达式…

秋名山老司机(详解)——bugku

刚刚做了bugku的题目&#xff0c;现在整理一下 写出解题思路&#xff0c;希望能够帮助到那些需要帮助的人 所有的wp都是以一题一篇的形式写出 主要是为了能够让读者更好的阅读以及查找&#xff0c; 希望你们不要责怪&#xff01;&#xff01;共勉&#xff01;&#xff01;&…

Bugku之秋名山老司机

秋名山老司机需要在2s内计算出来并提交&#xff0c;这个通过人工是不可能的&#xff0c;所以只能通过自己写脚本来计算并立即提交。 脚本如下也带有注释&#xff1a; import re import requestss requests.Session() r s.get("http://120.24.86.145:8002/qiumingshan…

bugKuctf-秋名山老司机

http://123.206.87.240:8002/qiumingshan/ 刷新几次发现需要把值post进去。 于是编辑脚本&#xff1a; encodingutf8 import re import requests s requests.Session() url ‘http://123.206.87.240:8002/qiumingshan/’ source s.get(url)#获取页面对象 expression re…

BugkuCTF: 秋名山老司机(web)

题目描述&#xff1a; 亲请在2s内计算老司机的车速是多少 1565348110-15858523191424136689-501596850-364488737*872756914-663618483-1120007195*1119001272-1463806595*1200528853?; 在两秒内刷新页面后会出现提示让提交计算出来的值&#xff0c;且url没有变化&#xff…