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

news/2024/5/4 6:11:39/文章来源:https://blog.csdn.net/LWR_Shadow/article/details/127113712

一、什么是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://www.luyixian.cn/news_show_16548.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

报告分享|数字化转型,从战略到执行报告

报告链接:http://tecdat.cn/?p=28672 如何加速国家、城市、行业、企业数字化进程,激发数字经济新动能。这份报告通过洞察数字化的6大改变、4大载体、4个阶段、20+场景、100+国家/项目案例/数据,全面系统性地阐述了多层次多场景数字化如何落地实施,最终带来经济、社会价值的…

报告分享|2022年企业数字化人才发展白皮书

报告链接:http://tecdat.cn/?p=28670 数字经济时代,企业对数字化人才的需求急剧增长。此报告对数字化人才培养和企业数字化人才发展现状进行梳理和研究,聚焦于金融、零售、能源和制造四个行业,采用定量与定性相结合的研究方法,对数字化人才的发展态势、岗位能力需求、培养…

第八章 常用用类

文章目录8.4 StringBuffer类8.4.1 StringBuffer对象8.4.2 StringBuffer类的常用方法1.append方法2.charAt(int n)和setCharAt(int n, char ch)8.5 Date类与Calendar类8.5.1 Date类8.5.2 Calendar类8.6 日期的格式变化8.6.1 format方法8.6.2 不同区域的星期格式8.7 Math类、BigI…

【算法】【二叉树模块】求一个二叉树“子树“是否包含另一个二叉树的全部拓扑结构

目录前言问题介绍解决方案代码编写java语言版本c语言版本c语言版本思考感悟写在最后前言 当前所有算法都使用测试用例运行过&#xff0c;但是不保证100%的测试用例&#xff0c;如果存在问题务必联系批评指正~ 在此感谢左大神让我对算法有了新的感悟认识&#xff01; 问题介绍 …

三个线程顺序打印ABC?我有十二种做法,彻底掌握多线程同步通信机制

大家好&#xff0c;我是老三&#xff0c;这篇文章分享一道非常不错的题目&#xff1a;三个线程按序打印ABC。 很多读者朋友应该都觉得这道题目不难&#xff0c;这次给大家带来十二种做法&#xff0c;一定有你没有见过的新姿势。 1. synchronizedwaitnotify 说到同步&#xf…

Swift中的内存访问冲突、指针、局部作用域

内存访问冲突&#xff08;Conflicting Access to Memory&#xff09; 1、内存访问冲突会在两个访问满足以下条件时发生&#xff1a; 至少一个是写入操作它们访问的是同一块内存它们的访问时间重叠&#xff08;比如在同一个函数内&#xff09; //无内存访问冲突 func plus(_ n…

PIE-engine 教程 ——利用NDWI加载青海湖三年水域影像和面积计算

这里我们首先画一个自己选择的研究区&#xff0c;用于方便计算NDWI&#xff0c;这里我们将青海湖区域作为我们的研究区&#xff0c;第二步我们就是要设定一个函数&#xff0c;用于在函数中执行循环遍历&#xff0c;这里包括去云和影像筛选过程&#xff0c;最后按照最大值合成&a…

Windows 10 docker 容器添加新端口映射的方法与步骤

在Docker容器已经创建后&#xff0c;需要添加新的端口映射&#xff0c;即对已经存在的Docker容器添加新的端口映射&#xff0c;可以通过以下步骤来添加&#xff0c;即通过修改配置文件的方法。 1、Windows 10 下 Dockers容器的配置文件存在的路径为&#xff1a; 笔者本文是20…

CLIP扩展

Audio CLIP:Extend CLIP to Image,Text and Audio&#xff08;语音&#xff09; 在已有的image、text 的基础上又加上了audio语音模态。 找了一些视频&#xff0c;有视频帧&#xff08;图像&#xff09;、文本、语音三种模态的信息&#xff0c;仿照CLIP的模型结构。三种模态两…

SpringSecurity + JWT(前后端分离)

文章目录一、先来聊聊 SpringSecurity JWT二、简单聊聊SpringSecurity 完整流程1、认证2、授权三、撸代码1、入门案例2、认证-前端端分离 Demo2.1 环境准备2.2 密码加密存储2.3 数据库校验存储2.4 编写自定义登陆接口2.5 JWT 认证过滤器2.6 退出登陆3、授权-前后端分离 Demo3.…

Spring In Action 5 学习笔记 chapter8 RabbitMQ(AMQP)要点

本文记录Sping In Action5 第8章 发送异步消息 RabbitMQ(AMQP)中的踩坑情况。 网搜的Spring In Action5的书籍在线翻译 https://potoyang.gitbook.io/spring-in-action-v5/ 第8章的源码请自行github或gitee搜索&#xff0c;或参考一下。 GitHub - habuma/spring-in-action-5…

web学生网页设计作业源码 HTML+CSS+JS 网上鲜花商城购物网站

&#x1f329;️ 精彩专栏推荐&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb;&#x1f447;&#x1f3fb; &#x1f482; 作者主页: 【进入主页—&#x1f680;获取更多源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;HTML5网页期末作业 (1000套…

MySQL Workbench 创建用户

创建本机用户 选择权限(增删改查)6个 创建新用户编辑窗口

【Web基础】Web应用体系结构 — 容器 + MVC设计模式

前言&#xff1a;提前祝大家国庆快乐了~~~~ 文章目录1 容器1.1 容器定义1.2 容器功能1.3 容器如何处理请求1.4 URL 映射 servlet2. MVC设计模式2.1 MVC 设计模式定义2.2 为什么要采用 MVC 设计模式&#xff1f;1 容器 1.1 容器定义 Servlet 没有 main() 方法&#xff0c;它们…

Vue再次入门<一>互动教程

两年前入了个门&#xff0c;很久没摸&#xff0c;又忘了。这次再来&#xff0c;改变下只做笔记的方式&#xff0c;改为边学边动手敲。加油&#xff0c;maymay&#xff5e; No one can stop u except yourself&#xff01; Vue再次入门一&#xff0c;互动教程1&#xff0c;声明式…

Windows11 家庭版开启远程桌面解决方案之RDP Wrapper Library,小白全面攻略

Windows11家庭版 首先需要了解的是Windows11是不支持家庭版的&#xff0c;那么既然看到了这里&#xff0c;那一定是具备解决方案的&#xff0c;方案就是通过 RDP Wrapper Library RDP软件下载 一定要下载合适的版本&#xff0c;最新的不一定是最合适的&#xff01; CSDN下载…

python代码学习——递归函数

python代码学习——递归函数函数的调用变量名解析&#xff08;查找&#xff09;原则&#xff1a;LEGB函数的执行流程函数调用的字节码递归&#xff08;Recursion&#xff09;斐波那契数列的递归实现方式递归的要求递归的性能斐波那契数列&#xff0c;递归方式的改进间接递归递归…

使用docker安装nginx笔记

一&#xff1a;查找镜像 docker search 名字 docker search nginx 二&#xff1a; 拉取nginx镜像到本地 (注&#xff1a;默认选取官方最新镜像)&#xff0c;其它版本可以去DockerHub查询 docker pull nginx 三&#xff1a;查看镜像 docker images nginx REPOSITORY&#xff…

Vue再次入门<二>深度指南

深度指南一&#xff0c;创建一个 Vue 应用1&#xff0c;应用实例2&#xff0c;根组件3&#xff0c;挂载应用4&#xff0c;DOM 中的根组件模板5&#xff0c;应用配置6&#xff0c;多个应用实例二&#xff0c;模板语法1&#xff0c;文本插值2&#xff0c;原始 HTML3&#xff0c;A…

python+vue在线学习教育平台django+flask

功能要求&#xff1a;可以实现首页、个人中心、学生管理、名师管理、课程名称管理、课程信息管理、计算机学院管理、设计学院管理、论坛管理、试卷管理、试题管理、系统管理、考试管理等功能模块。 本在线教育平台采用的数据库是Mysql&#xff0c;使用django框架技术开发。在设…