[本笔记参考Unity官方教程] https://blog.csdn.net/yhx956058885/category_10414478.html
Xlua热更新: https://www.aliyundrive.com/s/23tvqvAEnJz 提取码: 6g5d
1.什么热更新
软件的更新分为整包更新和热更新
整包更新(冷更新) : 为重新下载该程序包安装,体积较大,下载慢
热更新 : 为动态更新游戏内容,通过网络下载升级包,不需要发布新版本,升级包的体积比较小,下载速度快
热更新的方式可以绕过应用市场的审核,所以对于紧急的bug修复以及实时性较强的功能发布
2.热更新的方法
1.Lua系解决方案 :内置一个Lua虚拟机,做好UnityEngine与C#框架的Lua导出.典型的框架有XLua,Ulua,大体都差不多 求职必备
2.ILRuntime解决方案 :内置一个.net字节码解释器,解释执行.net字节码。
3.puerts解决方案 :内置一个JavaScript/TypeScript解释器,解释执行TypeScript代码。
4.HybridCLR(wolong) huatuo : 在IL2CPP VM中内置一个.net 字节码解释器,同时还会把.net里面的数据对象映射到native 的数据对象,huatuo的任何c#编写的代码都可以热更新 推荐学习,学习成本几乎零成本,一个特性完整、零成本、高性能、低内存的近乎完美的Unity全平台原生c#热更方案,其他方案则是 第三方解释器,与运行时不统一,最终结果是huatuo使用方便,兼容性好,性能和内存有极其巨大的优势
3.Xlua插件使用的语言
Lua语言 : 解释性语言 格式和python有点像 lua.exe 加入Lua到系统环境变量中去
可以看一下Lua的语法 Lua简单语法.pdf (大概花个一天时间过一下Lua语法最好)再来看接下来的步骤
XLua 是腾讯开发的一款用于 unity 热更新的插件,可以从github上下载源码 这里我已经提提前下好了 "Resources\xLua-master\Assets" 目录下导入Unity工程即可使用,如果出现抱错删除Xlua/Gen即可
https://github.com/Tencent/xLua (开源地址)可能需要加速器才可访问
开启热更新 : 进入(File/Build Setings.../Player Settings.../Player)后 ,找到 Scripting_Define Symbols 加上 HOTFIX_ENABLE 后 Apply,
然后查看Xlua下就出现了 Hotfix Inject In Editor
之后点击Clear Generated Code ==> Generate Code ==> Hotfix lnject ln Editor 就会重新帮助构建Gen目录,(注意:但凡是在更改了C#代码中有关于热更新部分的地方,就需要重新重复以上三个步骤)
4.了解Xlua中的一些类和API
LuaEnv类
创建一个Lua虚拟环境
// 成员方法
// 执行一个代码块
object[] DoString(string chunk, string chunkName = "chuck", LuaTable env = null);
// chunk : 表示执行的字符串
// chunkName : 发生error时的debug显示中使用,指名那行代码出错
// env : 这个代码的执行环境
// return : 返回这个chunk返回的值
// 加载一个代码块,但不执行,只返回指定为一个delegate 或者一个LuaFunction
T LoadString<T>(string chunk, string chunkName = "chunk", LuaTable env = null);
// chunk : 表示执行的字符串
// chunkName : 发生error时的debug显示中使用,指名那行代码出错
// env : 这个代码的执行环境
// return : 代表该代码块的delegate或者LuaFunction类
// 清除Lua的未手动释放的LuaBase对象(比如:LuaTable, LuaFunction),以及其它一些事情
void Tick();
// 增加一个自定义loader
void AddLoader(CustomLoader loader);
//loader:一个包含了加载函数的委托,其类型为delegate byte[] CustomLoader(ref string filepath),当一个文件被require时,这个loader会被回调,其参数是调用require所使用的参数,如果该loader找到文件,可以将其读进内存,返回一个byte数组。如果需要支持调试的话,而filepath要设置成IDE能找到的路径(相对或者绝对都可以)
// 释放掉Lua环境和一些资源
void Dispose();
// 字段
LuaTable Global; // lua全局环境的LuaTable 相当与 Lua中的 _G
使用建议:全局就一个实例,并在Update中调用GC(Tick)方法,完全不需要时调用Dispose
LuaTable类
用于模拟Lua的表
// 成员方法
T Get<T>(string key);
// 获取在key下,类型为T的value,如果不存在或者类型不匹配,返回null
T GetInPath<T>(string path);
// 和Get的区别是,这个函数会识别path里头的“.”,比如var i = tbl.GetInPath<int>(“a.b.c”)相当于在lua里头执行i = tbl.a.b.c,避免仅为了获取中间变量而多次调用Get,执行效率更高
void SetInPath<T>(string path, T val);
// 和GetInPaht<T>对应的setter
void Get<TKey, TValue>(TKey key, out TValue value);
// 上面的API的Key都只能是string,而这个API无此限制
void Set<TKey, TValue>(TKey key, TValue value);
// 对应Get<TKey, TValue>的setter
T Cast<T>();
// 把该table转成一个T指明的类型,可以是一个加了CSharpCallLua声明的interface,一个有默认构造函数的class或者struct,一个Dictionary,List等等
void SetMetaTable(LuaTable metaTable);
// 设置metaTable为table的metatable
LuaFunction类
注意:用该类访问Lua函数会有boxing,unboxing的开销,为了性能考虑,需要频繁调用的地方不要用该类。建议通过table.Get<ABCDelegate>获取一个delegate再调用(假设<ABCDelegate>是C#的一个delegate)。在使用使用table.Get<ABCDelegate>之前,请先把<ABCDelegate>加到代码生成列表
// 以可变参数调用Lua函数,并返回该调用的返回值
object[] Call(params object[] args);
// 调用Lua函数,并指明返回参数的类型,系统会自动按指定类型进行转换
object[] Call(object[] args, Type[] returnTypes);
// 相当于lua的setfenv函数 设置一个函数的执行环境
void SetEnv(LuaTable env)
LuaUserData类
对应非C# Managered对象的lua userdata 比如自定义结构体传入Lua
6.LuaAPI
CS对象
CS.命名空间.类() : 可以调用该类的构造函数并且返回一个示例对象
CS.命名空间.类.field : 访问该类的静态成员
CS.命名空间.enum.field : 访问一个枚举值
typeof 函数
类似C#里头的typeof关键字,返回一个Type对象
-- 给newGameObj添加一个叫CS.UnityEngine.ParticleSystem组件
newGameObj:AddComponent(typeof(CS.UnityEngine.ParticleSystem))
无符号 64 位支持
-- 无符号数转字符串
uint64.tostring()
-- 无符号数除法
uint64.divide()
-- 无符号比较,相等返回0,大于返回正数,小于返回负数
uint64.compare()
-- 无符号数取模
uint64.remainder()
-- 字符串转无符号数
uint64.parse()
xlua.structclone
克隆一个c#结构体
xlua.private_accessible(class)
让一个类的私有字段,属性,方法等可用
-- cast函数
-- 指明以特定的接口访问对象
a = cast(calc, typeof(CS.PerformentTest.ICalc))
-- calc对象实现了C#的PerformentTest.ICalc接口
-- a 可以调用该接口
建议:在Lua中定义变量时,尽量使用local本地变量,除非有想在C#中使用的变量
一些调用
local v1=CS.UnityEngine.Vector3(1,1,1)
local v2=CS.UnityEngine.Vector3(1,1,1)
v1.x = 100
v2.y = 100
print(v1, v2)
local v3 = v1 + v2
print(v1.x, v2.x)
print(CS.UnityEngine.Vector3.one)
print(CS.UnityEngine.Vector3.Distance(v1, v2))
7.类型映射
基本数据类型
C#类型 | Lua类型 |
---|---|
sbyte,byte,short,ushort,int,uint,double,char,float | number |
decimal | userdata |
long,ulong | userdata/lua_Integer(lua53) |
bytes[] | string |
bool | boolean |
string | string |
复杂数据类型
C#类型 | Lua类型 |
---|---|
LuaTable | table |
LuaFunction | function |
class或者 struct的实例 | userdata,table |
method,delegate | function |
class或者 struct的实例
从C#传一个class或者struct的实例,将映射到Lua的userdata,并通过__index访问该userdata的成员
C#侧指明从Lua侧输入指定类型对象,Lua侧为该类型实例的userdata可以直接使用;如果该指明类型有默认构造函数,Lua侧是table则会自动转换,转换规则是:调用构造函数构造实例,并用table对应字段转换到c#对应值后赋值各成员
method, delegate
C#侧的普通参数以及引用参数,对应lua侧函数参数;C#侧的返回值对应于Lua的第一个返回值;引用参数和out参数则按序对应于Lua的第2到第N个参数
8.Xlua的配置
打标签;静态列表;动态列表
① 打标签 [LuaCallCSharp]
[LuaCallCSharp]
public class A
{
}
// 该方式方便,但在il2cpp下会增加不少的代码量,不建议使用
② 静态列表
有时我们无法直接给一个类型打标签,比如系统api,没源码的库,或者实例化的泛化类型,据可以使用该方法打标签
[LuaCallCSharp]
public static List<Type> mymodule_lua_call_cs_list = new List<Type>()
{
typeof(GameObject),
typeof(Dictionary<string, int>),
};
// 表示为GamObject 和 Dictionary<string, int> 类配置[LuaCallCSharp]
// 这个字段需要放到一个静态类里头,建议放到Editor目录
③ 动态列表
声明一个静态属性,打上相应的标签即可
[Hotfix]
public static List<Type> by_property
{
get
{
return (from type in Assembly.GetExecutingAssembly().GetTypes()
where type.Namespace == "XXXX"
select type).ToList();
}
}
// 表示为
// 这个属性需要放到一个静态类里头,建议放到Editor目录
标签解释
XLua.LuaCallCSharp
一个C#类型加了这个配置,xLua会生成这个类型的适配代码(包括构造该类型实例,访问其成员属性、方法,静态属性、方法),否则将会尝试用性能较低的反射方式来访问。
一个类型的扩展方法(Extension Methods)加了这配置,也会生成适配代码并追加到被扩展类型的成员方法上。
xLua只会生成加了该配置的类型,不会自动生成其父类的适配代码,当访问子类对象的父类方法,如果该父类加了LuaCallCSharp配置,则执行父类的适配代码,否则会尝试用反射来访问。
反射访问除了性能不佳之外,在il2cpp下还有可能因为代码剪裁而导致无法访问,后者可以通过下面介绍的ReflectionUse标签来避免
XLua.ReflectionUse
一个C#类型类型加了这个配置,xLua会生成link.xml阻止il2cpp的代码剪裁
建议所有要在Lua访问的类型,要么加LuaCallCSharp,要么加上ReflectionUse,这才能够保证在各平台都能正常运行
XLua.CSharpCallLua
如果希望把一个lua函数适配到一个C# delegate(一类是C#侧各种回调:UI事件,delegate参数,比如List<T>:ForEach;另外一类场景是通过LuaTable的Get函数指明一个lua函数绑定到一个delegate)。或者把一个lua table适配到一个C# interface,该delegate或者interface需要加上该配置。
XLua.GCOptimize
除枚举之外,包含无参构造函数的复杂类型,都会生成lua table到该类型,以及改类型的一维数组的转换代码,这将会优化这个转换的性能,包括更少的gc alloc
默认情况下GCOptimize只对public的field打解包
XLua.AdditionalProperties
这个是GCOptimize的扩展配置,有的时候,一些struct喜欢把field做成是私有的,通过property来访问field
要求是Dictionary<Type, List<string>>类型,Dictionary的Key是要生效的类型,Value是属性名列表
XLua.BlackList
不要生成一个类型的一些成员的适配代码
// 例如下面是对GameObject的一个属性以及FileInfo的一个方法列入黑名单
[BlackList]
public static List<List<string>> BlackList = new List<List<string>>() {
new List<string>(){"UnityEngine.GameObject", "networkView"},
new List<string>(){"System.IO.FileInfo", "GetAccessControl", "System.Security.AccessControl.AccessControlSections"},
};
生成期配置 必须放入Editor目录下
CSObjectWrapEditor.GenPath
配置生成代码的放置路径,类型是string。默认放在“Assets/XLua/Gen/”下
CSObjectWrapEditor.GenCodeMenu
该配置用于生成引擎的二次开发,一个无参数函数加了这个标签,在执行“XLua/Generate Code”菜单时会触发这个函数的调用
9.Lua文件加载 自定义加载器
using UnityEngine;
using XLua;
using System.IO;
public class First : MonoBehaviour
{
LuaEnv env;
void Start()
{
// 创建运行环境
env = new LuaEnv();
// 添加加载器 否则Xlua会从默认路径找
env.AddLoader(MyLoader);
// env环境执行"require 'test00'"lua代码
env.DoString("require 'test00'");
}
// 自定义加载器
byte[] MyLoader(ref string filePath)
{
// 获取工程地址
string projectPath = Application.dataPath;
print(projectPath);
// 获取存放Lua脚本的目录
filePath = projectPath.Substring(0, projectPath.Length - 7) + "/Lua/" + filePath + ".lua";
print(filePath);
// 读取该lua脚本转化为byte[]
return File.ReadAllBytes(filePath);
}
void Update()
{
// 对资源进行回收
env.Tick();
}
private void OnDestroy()
{
// 销毁运行环境 回收资源
env.Dispose();
}
}
10.C#访问lua
C#主动发起对Lua数据结构的访问
一.获取一个全局基本数据类型 LuaEnv.Global + Get方法
env.Global.Get<int>("a");
env.Global.Get<string>("b");
env.Global.Get<bool>("a");
二.访问一个全局的table
也是通过Get来获取,但是需要指定泛型
1.自己来一个对应的class或者struct来接受 (需要有无参构造)
这个过程是值拷贝,如果class比较复杂代价会比较大。而且修改class的字段值不会同步到table,反过来也不会
这个功能可以通过把类型加到GCOptimize生成降低开销
2.映射到一个interface
这种方式依赖于生成代码(如果没生成代码会抛InvalidCastException异常),代码生成器会生成这个interface 的实例,如果get一个属性,生成代码会get对应的table字段,如果set属性也会设置对应的字段。甚至可以通过 interface的方法访问lua的函数
3、更轻量级的by value方式:映射到Dictionary<>,List<>
不想定义class或者interface的话,可以考虑用这个,前提table下key和value的类型都是一致的
4、另外一种by ref方式:映射到LuaTable类
这种方式好处是不需要生成代码,但也有一些问题,比如慢,比方式2要慢一个数量级,比如没有类型检查
三.访问一个全局的function
仍然是用Get方法,不同的是类型映射
1、映射到delegate
这种是建议的方式,性能好很多,而且类型安全。缺点是要生成代码
多返回值要怎么处理?
从左往右映射到c#的输出参数,输出参数包括返回值,out参数,ref参数
2、映射到LuaFunction
这种方式的优缺点刚好和第一种相反 慢 无需生成代码
LuaFunction上有个变参的Call函数,可以传任意类型,任意个数的参数,返回值是object的数组,对应于lua的多返回值
四.使用建议
1、访问lua全局数据,特别是table以及function,代价比较大,建议尽量少做,比如在初始化时把要调用的lua function获取一次(映射到delegate)后,保存下来,后续直接调用该delegate即可。table也类似。
2、如果lua侧的实现的部分都以delegate和interface的方式提供,使用方可以完全和xLua解耦:由一个专门的模块负责xlua的初始化以及delegate、interface的映射,然后把这些delegate和interface设置到要用到它们的地方。
五.例子
using UnityEngine;
using XLua;
using System.IO;
using System;
public class First : MonoBehaviour
{
LuaEnv env;
Action ac0;
delegate void OneTest(object a);
OneTest ac1;
// 两个及以上参数的函数需要手动配置
Action<int, int> ac2;
Func<int, int, int> ac3;
// 由于XLua没有开启自动适配,需要手动配置
[CSharpCallLua] // 注意delegate需要设置为public才可以设置[CSharpCallLua]
public delegate void TestFunc(int a, int b, int c, out int d, out int e);
TestFunc ac4 = null;
LuaFunction ac5 = null;
// Start is called before the first frame update
void Start()
{
// 创建运行环境
env = new LuaEnv();
env.AddLoader(MyLoader);
env.DoString("require 'test00'");
print(env.Global["name"]);
print(env.Global["age"]);
print(env.Global["sex"]);
ac0 = env.Global.Get<Action>("TestFunc");
ac1 = env.Global.Get<OneTest>("OneTestFunc");
ac2 = env.Global.Get<Action<int, int>>("Add");
ac3 = env.Global.Get<Func<int, int, int>>("Sub");
ac4 = env.Global.Get<TestFunc>("TestMoreFunc");
ac5 = env.Global.Get<LuaFunction>("LuaFunctionTest");
ac0();
ac1(12);
ac2(10, 10);
print(ac3(20, 100));
int d, e;
ac4(1, 2, 3, out d, out e);
print("d : " + d);
print("e : " + e);
print(ac5.Call(100)[0]);
print("-------------映射Lua table--------------");
print("-------映射到普通class或struct-----------");
// 需要准备相对应的class 或者 struct
// class struct 成员需要公开
CSharpCallLuaTableTest00 test00 = env.Global.Get<CSharpCallLuaTableTest00>("CSharpCallLuaTableTest00");
print(test00.name);
print(test00.age);
print(test00.address);
// 修改其中一个的值
test00.address = "成都";
env.DoString("print(CSharpCallLuaTableTest00.address)");
// Lua中结果没有改变,该方式是值传递,只修改C#中的数据
print("-------------------END------------------");
print("-------------映射到interface----------------");
ICSharpCallLuaTableTest00 Itest00 = env.Global.Get<ICSharpCallLuaTableTest00>("CSharpCallLuaTableTest00");
print(Itest00.name);
print(Itest00.age);
print(Itest00.address);
Itest00.name = "小量";
env.DoString("print(CSharpCallLuaTableTest00.name)");
// Lua中结果改变了,该方式是引用拷贝,双方都要修改
print("-------------------END------------------");
print("-------------映射到interface采用复杂的表----------------");
ICSharpCallLuaTableTest01 Itest01 = env.Global.Get<ICSharpCallLuaTableTest01>("CSharpCallLuaTableTest01");
print("bookName : " + Itest01.bookName);
print("price : " + Itest01.price);
Itest01.ReadBook();
Itest01.LendBook(200.0f);
print("-------------------END------------------");
print("-------------------通过Dictionary或者List------------------");
// 使用dictionary映射的方式适合映射键值对类型的表
Dictionary<string, object> test02 = env.Global.Get<Dictionary<string, object>>("CSharpCallLuaTableTest00");
foreach(string key in test02.Keys)
{
print("Key : " + key + " Value : " + test02[key]);
}
test02["name"] = "阿达";
env.DoString("print(CSharpCallLuaTableTest00.name)");
// Lua中结果未改变,该方式是值拷贝,C#要修改
print("-------------------END------------------");
print("-------------映射到List方式----------------");
// 使用List映射的方式适合映射数组类型的表
List<string> stdentListtest00 = env.Global.Get<List<string>>("studentList");
for(int i = 0; i < stdentListtest00.Count; i++)
{
print(stdentListtest00[i]);
}
stdentListtest00[0] = "长春";
env.DoString("print(studentList[1])");
// Lua中结果未改变,该方式是值拷贝,C#要修改
print("-------------------END------------------");
print("----------------通过LuaTable------------------");
// 这种方式的优点是不需要生成代码,确定是比较慢(效率低下),比interface方式要慢一个数量级
// 所以这种方式不推荐常用,适合用在一些比较复杂且使用频率很低的情况
LuaTable luaTableTest00 = env.Global.Get<LuaTable>("CSharpCallLuaTableTest01");
print(luaTableTest00.Get<string>("bookName"));
print(luaTableTest00.Get<int>("price"));
LuaFunction luaTableFunctionAc0 = luaTableTest00.Get<LuaFunction>("ReadBook");
luaTableFunctionAc0.Call(luaTableTest00);
LuaFunction luaTableFunctionAc1 = luaTableTest00.Get<LuaFunction>("LendBook");
luaTableFunctionAc1.Call(luaTableTest00, 100);
}
// 自定义加载器
byte[] MyLoader(ref string filePath)
{
// 获取工程地址
string projectPath = Application.dataPath;
// 获取存放Lua脚本的目录
filePath = projectPath.Substring(0, projectPath.Length - 7) + "/Lua/" + filePath + ".lua";
// 读取该lua脚本转化为byte[]
return File.ReadAllBytes(filePath);
}
// Update is called once per frame
void Update()
{
// 对资源进行回收
env.Tick();
}
private void OnDestroy()
{
// 释放资源
ac0 = null;
ac1 = null;
ac2 = null;
ac3 = null;
ac4 = null;
ac5 = null;
// 销毁运行环境 回收资源
env.Dispose();
}
}
-- GameObject =CS.UnityEngine.GameObject
-- Debug=CS.UnityEngine.Debug
-- Vector3=CS.UnityEngine.Vector3
print("------------------C#访问lua--------------------")
name = "小明"
age = 12
sex = "男"
function TestFunc()
print("测试0个参数")
end
function OneTestFunc(a)
print(a)
end
function Add(a,b)
print(a+b)
end
function Sub(a,b)
return a-b
end
function TestMoreFunc(a,b,c)
print(a,b,c)
return a+b,b+c
end
function LuaFunctionTest(a)
print(a)
return a
end
CSharpCallLuaTableTest00 = {
name = "夏明",
age = 12,
address = "上海"
}
CSharpCallLuaTableTest01 = {
bookName = "今天也要在异世界努力生存下去",
price = 100,
ReadBook = function (self)
print("阅读 " ..self.bookName .. " ing")
end,
LendBook = function (self,lendBookPrice)
print("借出 " ..self.bookName .. " ing")
print("价格为 : " .. lendBookPrice)
end
}
studentList = {"小明","小静","常量","凯斯"}
// 配置脚本
// 推荐写在Editor目录下
using System;
using System.Collections.Generic;
using XLua;
public static class CSharpCallLuaTest
{
[CSharpCallLua]
public static List<Type> CSharpCallLua = new List<Type>()
{
// 对需要适配Lua中的function进行配置
typeof(Action<int,int>),
typeof(Func<int, int, int>),
};
}
Lua调用C#
C# : var newGameObj = new UnityEngine.GameObject(); ===> Lua : local newGameObj = CS.UnityEngine.GameObject()
Lua中没有new关键字
和C#相关的都放在了CS下 包括 构造 静态成员 方法
Xlua支持部分重载
读取静态属性
用点的方式
CS.UnityEngine.Debug.Log("Hello World")
写静态属性
同读一样,点出来直接修改就行
CS.UnityEngine.Time.timeScale = 0.2
调用静态方法
CS.UnityEngine.GameObject.Find("newObject")
Tips:如果需要经常访问的类,可以先用局部变量引用后访问,除了减少敲代码的时间,还能提高性能
GameObject = CS.UnityEngine.GameObject Debug=CS.UnityEngine.Debug Vector3=CS.UnityEngine.Vector3
访问成员属性.方法
掉用成员属性用点
调用成员方法用冒号
父类属性,方法
xlua支持(通过派生类)访问基类的静态属性,静态方法,
(通过派生类实例)访问基类的成员属性,成员方法
参数的输入输出属性(out,ref)
Lua侧调用C#:C#的普通参数算一个输入,ref算输入,out不算
C#得到Lua的返回值:return 算一个 out 算一个 ref 算一个 ,对应从左往右返回给C#
重载
Xlua支持一定程度上的重载 eg : C# :int,float,double ==> Lua:number
操作符
支持的操作符有:+,-,*,/,==,一元-,<,<=, %,[]
参数带默认值的方法
和C#一样,如果实参少于形参,会自动补上
可变参数
C# : void VariableParamsFunc(int a, params string[] strs)
Lua : testobj:VariableParamsFunc(5, 'hello', 'john')
使用Extension methods C#类方法扩展
Lua可以直接调用
泛化(模版)方法
不直接支持,可以通过Extension methods功能进行封装后调用
枚举类型
属于枚举类型下的静态属性一样,用点就行了
如果枚举类加入到生成代码的话,枚举类将支持__CastFrom方法,可以实现从一个整数或者字符串到枚举值的转换
CS.Tutorial.TestEnum.__CastFrom('E1')
表示把TestEnum下一个叫E1“的枚举类型转化为枚举值
delegate使用(调用,+,-)
C#的delegate调用:和调用普通lua函数一样
操作符:对应C#的+操作符,把两个调用串成一个调用链,右操作数可以是同类型的C# delegate或者是lua函数。
-操作符:和+相反,把一个delegate从调用链中移除。
Ps:delegate属性可以用一个luafunction来赋值
event
public event Action TestEvent
增加事件回调:class:TestEvent('+',function)
移除事件回调:class:TestEvent('-',function)
64位整型支持
Lua53原生有64位整型,直接映射
luajit版本:本身不支持64位,xlua做了个64位支持的扩展库,C#的long和ulong都将映射到userdata
C#复杂类型和table的自动转换
遇见类/结构体就大括号进行构造,普通的变量类型直接添值就行
获取类型(相当于C#的typeof)
typeof(CS.UnityEngine.ParticleSystem)
“强”转
cast(calc, typeof(CS.Tutorial.Calc))
上面就是指定用CS.Tutorial.Calc的生成代码来访问calc对象
例子
using System;
using System.IO;
using UnityEngine;
using XLua;
namespace Test
{
public class LuaCallCsharp00
{
public static string name = "小明";
public static int age = 12;
public static void Speak()
{
Debug.Log("Hello!");
}
public string address = "仁寿";
public virtual void Dosomething()
{
Debug.Log("DoSomeThing...");
}
public LuaCallCsharp00()
{
}
public LuaCallCsharp00(string address)
{
this.address = address;
}
}
public class LuaCallCsharp01 : LuaCallCsharp00
{
public void Dosomething()
{
Debug.Log("子类 DoSomeThing...");
}
public void Dosomething(string str)
{
Debug.Log(str);
}
public void Dosomething(int price)
{
Debug.Log("买菜 Price : " + price);
}
public void Dosomething(string boName,float price)
{
Debug.Log($"买书 {boName} 的 Price : " + price);
}
}
public class Person
{
public void Method(Student student)
{
Debug.Log($"{student.name} {student.age} {student.address} {student.ranking}");
Debug.Log(student.level);
}
public void Method(IStudent student,int a)
{
Debug.Log($"{student.name} {student.age} {student.address} {student.ranking}");
Debug.Log(student.level);
student.Speak();
student.Dosomething();
student.Dosomething1("asasa");
}
public void Method1(StudentDelegate sdel)
{
sdel.Invoke(new Student() { name = "大王",age = 30,ranking = 12,address = "广州",level = Level.three });
}
}
public struct Student
{
public string name;
public int age;
public int ranking;
public string address;
public Level level;
}
public enum Level
{
one,
two,
three
}
[CSharpCallLua]
public interface IStudent
{
string name { set; get; }
int age { set; get; }
int ranking { set; get; }
string address { set; get; }
Level level { set; get; }
void Speak();
void Dosomething();
void Dosomething1(string str);
}
[CSharpCallLua]
public delegate void StudentDelegate(Student student);
// Lua通过扩展方法掉用泛型
[LuaCallCSharp]
public class MyGengerric
{
public T GetMax<T>(T num1, T num2) where T : IComparable
{
return (num1.CompareTo(num2) < 0) ? num2 : num1;
}
}
[LuaCallCSharp]
public static class Extension_MyGengerric
{
public static int GetMax(this MyGengerric gen, int num1, int num2)
{
return (num1 > num2) ? num1 : num2;
}
}
}
public class LuaCallCsharpTest : MonoBehaviour
{
LuaEnv env;
private void Awake()
{
env = new LuaEnv();
env.AddLoader(MyLoader);
env.DoString("require 'LuaCallCSharp'");
}
// 自定义加载器
byte[] MyLoader(ref string filePath)
{
// 获取工程地址
string projectPath = Application.dataPath;
// 获取存放Lua脚本的目录
filePath = projectPath.Substring(0, projectPath.Length - 7) + "/Lua/" + filePath + ".lua";
// 读取该lua脚本转化为byte[]
return File.ReadAllBytes(filePath);
}
void Update()
{
// 对资源进行回收
env.Tick();
}
private void OnDestroy()
{
env.Dispose();
}
}
local Input = CS.UnityEngine.Input
local TouchPhase = CS.UnityEngine.TouchPhase
local Mathf = CS.UnityEngine.Mathf
local Quaternion = CS.UnityEngine.Quaternion
local Vector2 = CS.UnityEngine.Vector2
local Vector3 = CS.UnityEngine.Vector3
GameObject =CS.UnityEngine.GameObject
Debug=CS.UnityEngine.Debug
Vector3=CS.UnityEngine.Vector3
print("---------Lua Call C#-----------")
print("掉用静态成员")
print(CS.Test.LuaCallCsharp00.name)
print(CS.Test.LuaCallCsharp00.age)
CS.Test.LuaCallCsharp00.age = 20
print("修改后的age : " ..CS.Test.LuaCallCsharp00.age)
print("掉用静态方法")
CS.Test.LuaCallCsharp00.Speak()
-- 创建LuaCallCsharp00对象,加 ()
local test00 = CS.Test.LuaCallCsharp00('青岛')
print(CS.Test.LuaCallCsharp00.age)
-- 调用成员字段
print(test00.address)
test00.address = "成都"
print(test00.address)
-- 调用成员方法
test00:Dosomething()
-- 调用父类方法
local test01 = CS.Test.LuaCallCsharp01()
-- 调用父类字段
print(test01.address)
-- 调用子类重写的方法
test01:Dosomething()
-- 函数重载调用
test01:Dosomething("做家务")
test01:Dosomething(30)
test01:Dosomething("世界遗忘了我之后,我找到了通向世界尽头的路",49.99)
-- 调用带有复杂的参数
local person = CS.Test.Person()
local student00 = {name = "小强",age = 17,ranking = 30,address = "沁阳",level = CS.Test.Level.two}
person:Method(student00)
-- 带有接口的参数
local Iperson = {
name = "小钱",
age = 16,
ranking = 2,
address = "沁阳",
level = CS.Test.Level.one,
Speak = function ()
print("Hello!")
end,
Dosomething = function ()
print("做点事情")
end,
Dosomething1 = function (self,str)
print(str)
end
}
person:Method(Iperson,1)
-- 调用带有委托参数的函数
sdel = function (stu)
print(stu.name)
print(stu.age)
print(stu.ranking)
print(stu.address)
print(stu.level)
end
person:Method1(sdel)
-- 调用泛型方法
-- lua不支持直接调用c#中的泛型方法,但是可以通过扩展方法功能进行封装后调用
local maxNum = CS.Test.MyGengerric():GetMax(10,20)
print("maxNum:"..maxNum)
AssetsBundle
1.什么是AB包
特定于平台的资产压缩包,类似与压缩包 == > 不能压缩C#文件
资产包括:模型,贴图,预设体,音效,材质球,动作等等
2.了解AB包的作用
相对于Resources文件夹而言:Resources在打包时定死的,只可读,无法修改
AB包:存储文件可以自定义路径,可以压缩空间,可以从网络上下载,热更新的基础,比Resources灵活
减少初始包的大小
热更新
资源热更新
脚本热更新
3.生成AB包资源文件
1.自定义打包工具
2.Unity提供的Assets Bundle Browser
"com.unity.assetbundlebrowser": "1.7.0"
如果没有搜到该安装包,就在Packages/manifest.json文件中添加上上面的那一串字符串
把资源进行打包
1.拖成预制体
2.在Inspector面板的最下面new一个写名称,右边那个为文件的后缀名
3.Window->AssetBundle Browser打开AssetBundle
4.Configure会出现new的物体
5.Build 面板中 Build Target 为打包到的平台
Output Path : AB包的路径,在该工程文件下创建的
Clear Folders:每次打包是否清除文件夹
Copy to StreamingAssets : 是否复制AB包的文件拷贝到StreamingAssets 下
Compression : NC : 不压缩,压缩文件最大
LZMA :压缩文件最小,但是解压缩时需要全部解压才能用
LZ4 : 压缩打包,需要那个就解压那个,内存占用低
EIT:在资源包中 不包含资源的类型文件
FR:重新打包时需要重新构建包,和Clear Folders很像,但是不会删除
不存在的包
ITTC:增量构建检查时,忽略类型的更改
Append Hash:将文件Hash值附加到资源包上
SM:严格模式,如果打包报错了,则打包失败无法成功
DRB:运行时构建
打包文件类型:
有manifst后缀的文件为对应资源文件的依赖
没有的manifst后缀的为资源文件
会有一个和该目录名一样的文件 主包
该文件带有manifst的文件带有所有文件相对与那个包有依赖的信息
6.Inspect面板:查看文件大小
4.加载AB包和AB包资源
using System.Collections;
using UnityEngine;
public class ABundleTEst : MonoBehaviour
{
public Transform Parent;
// Start is called before the first frame update
void Start()
{
// 同步加载
// 加载 AB包
AssetBundle ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/goodsitem");
// 记载 AB包中的资源
// 用名字加载会出现同名不同类型的资源
// 建议使用泛型加载 或者时 指定Type加载
//GameObject obj = ab.LoadAsset<GameObject>("goodsItem");
GameObject obj = ab.LoadAsset("goodsItem", typeof(GameObject)) as GameObject;
Instantiate(obj, Parent);
// AB包不能加载两次,再次加载需要先卸载之前加载的AB包
// true : 卸载AB包后在场景下的游戏物体会收到影响
// false : 卸载AB包后在场景下的游戏物体不会收到影响
ab.Unload(false); // 卸载goodsitem AB包
AssetBundle.UnloadAllAssetBundles(false); // 卸载所有的AB包
ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/goodsitem"); // 再加载
ab.Unload(false); // 再卸载
// 异步加载 用到携程
// StartCoroutine(LoadRes("goodsitem", "goodsitem"));
// 依赖关系的问题
// 如果一个包中的资源依赖另一个包中的资源
// 需要手动把另外依赖的包给加载进来
// 就可以依赖主包得到包和包之间的依赖信息
// 加载 主包
AssetBundle abmain = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "PC");
// 加载主包中的固定文件
AssetBundleManifest manifest = abmain.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
// 得到固定文件中的依赖信息
string[] args = manifest.GetAllDependencies("cube");
Debug.Log(args.Length);
foreach (string arg in args)
{
// 打印出cube包依赖的其他包的名称
Debug.Log(arg);
}
ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "cube");
obj = ab.LoadAsset("Cube", typeof(GameObject)) as GameObject;
Instantiate(obj);
ab.Unload(false);
ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "goodsitem");
}
IEnumerator LoadRes(string abName,string objName)
{
// 加载AB包
AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + abName);
yield return abcr;
// 加载资源
AssetBundleRequest abr = abcr.assetBundle.LoadAssetAsync(objName,typeof(GameObject));
yield return abr;
GameObject obj = abr.asset as GameObject;
Instantiate(obj, Parent);
AssetBundle.UnloadAllAssetBundles(false); // 卸载所有的AB包
}
}
可以自己试着写一个加载AssetBundle的封装脚本
纯Lua案例:游戏背包界面(简单版)
由于用Lua开发,那么就需要一个Lua文件来作为所有Lua文件打开的入口 ,我们把该文件命名为mian.lua,来作为所有lua文件的管理者,以后需要新建lua文件就在该文件下 "require 'lua文件'" 就可以了,由于是案例,不会去实现复杂的功能,不会使用难度大的脚本,主要为巩固Xlua的Lua调用C#,如果纯C#可以开发了,那么办C#半Lua开发就简单多了,由于案例比较简单为了方便看,我把写的代码放入同一个脚本中了,但是自己在开发时一定要把脚本分类好,这样有利于自己和他人阅读
using System.IO;
using UnityEngine;
using XLua;
public class Demo : MonoBehaviour
{
LuaEnv env;
private void Start()
{
env = new LuaEnv();
env.AddLoader(MyLoader);
env.DoString("require 'main'");
}
private void Update()
{
env.Tick();
}
private void OnDestroy()
{
env.DoString("ClearChache()");
env.Dispose();
env = null;
}
private byte[] MyLoader(ref string filePath)
{
filePath = Application.dataPath.Substring(0, Application.dataPath.Length - 7) + "/Lua/" + filePath + ".lua";
print(filePath);
return File.ReadAllBytes(filePath);
}
}
-- 简化一些语句
local GameObject = CS.UnityEngine.GameObject
local UI = CS.UnityEngine.UI
local AssetBundle = CS.UnityEngine.AssetBundle
local Application = CS.UnityEngine.Application
local Sprite = CS.UnityEngine.Sprite
-- 图标
local pc1 = nil
local pc2 = nil
local pc3 = nil
local pc4 = nil
local pc5 = nil
-- 找到需要生成图标的父对象
local grid = GameObject.Find("Grid")
grid:SetActive(false)
-- 找到关闭按钮
local close = GameObject.Find("Close")
close:SetActive(false)
-- 找到数据面板
local info = GameObject.Find("Info")
info:SetActive(false)
local bagButton = GameObject.Find("BagButton"):GetComponent(typeof(UI.Button))
local goodsit = nil
-- 加载AssetBundle
local LoadRes = function()
-- 加载图片资源
abpic = AssetBundle.LoadFromFile(Application.streamingAssetsPath .."/ui")
pc1 = abpic:LoadAsset('goods (1)',typeof(Sprite))
pc2 = abpic:LoadAsset('goods (2)',typeof(Sprite))
pc3 = abpic:LoadAsset('goods (3)',typeof(Sprite))
pc4 = abpic:LoadAsset('goods (4)',typeof(Sprite))
pc5 = abpic:LoadAsset('goods (5)',typeof(Sprite))
abpic:Unload(false)
-- 加载背包槽资源
absolt = AssetBundle.LoadFromFile(Application.streamingAssetsPath .."/goodsitem")
goodsit = absolt:LoadAsset('goodsItem',typeof(GameObject))
cast(goodsit,typeof(GameObject))
absolt:Unload(false)
end
-- 关闭背包
local CloseBag = function()
grid:SetActive(false)
close:SetActive(false)
info:SetActive(false)
end
-- 解除绑定
local unBind = function ()
local maxIndex = grid.transform.childCount
for i = 0,maxIndex-1 do
local temp = grid.transform:GetChild(i)
temp:GetComponent(typeof(UI.Button)).onClick = nil
end
end
-- 打开背包
local OpenBag = function()
if(grid.transform.childCount == 0) then
if(not grid.activeSelf) then
-- 添加物品
local temp1 = GameObject.Instantiate(goodsit,grid.transform):GetComponent(typeof(UI.Image))
temp1.sprite = pc1
local temp2 = GameObject.Instantiate(goodsit,grid.transform):GetComponent(typeof(UI.Image))
temp2.sprite = pc3
local temp3 = GameObject.Instantiate(goodsit,grid.transform):GetComponent(typeof(UI.Image))
temp3.sprite = pc5
temp1:GetComponent(typeof(UI.Button)).onClick:AddListener(function ()
info:SetActive(true)
info.transform:GetChild(1):GetComponent(typeof(UI.Text)).text = "temp1"
end)
temp2:GetComponent(typeof(UI.Button)).onClick:AddListener(function ()
info:SetActive(true)
info.transform:GetChild(1):GetComponent(typeof(UI.Text)).text = "temp2"
end)
temp3:GetComponent(typeof(UI.Button)).onClick:AddListener(function ()
info:SetActive(true)
info.transform:GetChild(1):GetComponent(typeof(UI.Text)).text = "temp3"
end)
end
end
grid:SetActive(true)
close:SetActive(true)
end
LoadRes()
bagButton.onClick:AddListener(OpenBag)
close:GetComponent(typeof(UI.Button)).onClick:AddListener(CloseBag)
-- 释放资源
function ClearChache()
print("------------释放资源------------")
bagButton.onClick = nil
close:GetComponent(typeof(UI.Button)).onClick = nil
unBind()
CloseBag = nil
OpenBag = nil
unBind = nil
end
用Unity打开即可,本案例旨在了解,熟悉Lua掉用C#的API,标准写法还需请自行上网查询
搭建一个本地服务器(自己测试用)实现简单得热更新功能
1.安装一下NeBox,配置好环境变量
2.在工程目录下新建目录NetBox,新建文件localNet.box,使用记事本打开输入以下字符串
Dim httpd Shell.Service.RunService "NBWeb", "NetBox Web Server", "NetBox Http Server Sample" '---------------------- Service Event --------------------- Sub OnServiceStart() Set httpd = NetBox.CreateObject("NetBox.HttpServer") If httpd.Create("", 8009) = 0 Then Set host = httpd.AddHost("", "") host.EnableScript = true host.AddDefault "1.html" httpd.Start else Shell.Quit 0 end if End Sub Sub OnServiceStop() httpd.Close End Sub Sub OnServicePause() httpd.Stop End Sub Sub OnServiceResume() httpd.Start End Sub
3.新建一个文件,取名为 1.html,用来测试是否成功配置
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
4.在浏览器输入 (http://localhost:8009/1.html),出现Hello World表示配置成功
5.Unity中 File->Build Settings->Player Settings->Player->Other Settings->Scripting Define Symbols」选项中添加`HOTFIX_ENABLE
6.把上面案例下得Lua文件夹和工程目录下的AssetBundles文件夹放入NetBox目录下
7.添加一个场景,用于通过从本地服务器下载文件到对应的自定义加载Lua文件夹下
using System.Collections;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
public class CheckLoadRes : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
StartCoroutine(LoadRes());
}
IEnumerator LoadRes()
{
// 从本地下载Lua资源文件
WWW www = new WWW("http://localhost:8009/Lua/main.lua");
yield return www;
string targetDir = Application.dataPath.Substring(0,Application.dataPath.Length - 7) + "/Lua";
File.WriteAllText(targetDir + "/main.lua", www.text);
SceneManager.LoadScene(1);
}
}
8.给需要热更新的类加上[Hotfix]标签后,就可以在Lua中指定需要替换/增加的方法
local Demo = CS.Demo
-- 允许访问私有成员
-- 热更新替换
xlua.private_accessible(Demo)
xlua.hotfix(Demo,"Update",function(self)
if(Input.GetKeyDown(CS.UnityEngine.KeyCode.Space)) then
local temp1 = GameObject.Instantiate(goodsit,grid.transform):GetComponent(typeof(UI.Image))
temp1.sprite = pc1
temp1:GetComponent(typeof(UI.Button)).onClick:AddListener(function ()
info:SetActive(true)
info.transform:GetChild(1):GetComponent(typeof(UI.Text)).text = "新生成的temp1"
end)
end
end)
-- 热更新增量
-- 只能增量C#,无法增量在Lua中修改的代码,原Lua替换的代码会被增量代码提换
-- local util = require 'util'
-- util.hotfix_ex(Demo,"Update",function(self)
-- -- 调用原本的方法
-- print('这是增量代码')
-- end)
在结束释放资源时,需要释放热更新资源
function ClearChache()
print("------------释放资源------------")
bagButton.onClick = nil
close:GetComponent(typeof(UI.Button)).onClick = nil
unBind()
CloseBag = nil
OpenBag = nil
unBind = nil
-- 释放热更新资源
xlua.hotfix(CS.Demo,'Update',nil)
end
9.资源热更 把通过使用unity提供的打包工具打包的AB包放到服务器中去
从服务器下载AssetsBundles(AB包)资源包到StreamingAssets目录下
IEnumerator LoadABRes(string fileName)
{
string DownLoadAddress = "http://localhost:8009/AssetBundles/PC";
string DownLoadToAddress = Application.streamingAssetsPath;
WWW request = new WWW(DownLoadAddress + "/" + fileName);
yield return request;
File.WriteAllBytes(DownLoadToAddress + "/" + fileName, request.bytes);
request = new WWW(DownLoadAddress + "/" + fileName + ".manifest");
yield return request;
File.WriteAllBytes(DownLoadToAddress + "/" + fileName + ".manifest", request.bytes);
print("资源加载完毕");
}
private void Update()
{
if(Input.GetKeyDown(KeyCode.Space))
{
// 完成加载后点击空格切换场景
SceneManager.LoadScene(1);
}
}
10.在脚本热更中使用新加载的资源包
-- 加载AssetBundle
local LoadRes = function()
-- 加载图片资源
local abpic = AssetBundle.LoadFromFile(Application.streamingAssetsPath .."/ui")
pc1 = abpic:LoadAsset('goods (1)',typeof(Sprite))
pc2 = abpic:LoadAsset('goods (2)',typeof(Sprite))
pc3 = abpic:LoadAsset('goods (3)',typeof(Sprite))
pc4 = abpic:LoadAsset('goods (4)',typeof(Sprite))
pc5 = abpic:LoadAsset('goods (5)',typeof(Sprite))
-- 加载背包槽资源
local absolt = AssetBundle.LoadFromFile(Application.streamingAssetsPath .."/goodsitem")
goodsit = absolt:LoadAsset('goodsItem',typeof(GameObject))
-- 加载cube模型资源包
local cube = AssetBundle.LoadFromFile(Application.streamingAssetsPath .."/cube")
-- 解压取出 Cube 和 Sphere
Cube = cube:LoadAsset('Cube',typeof(GameObject))
Sphere = cube:LoadAsset('Sphere',typeof(GameObject))
end
local Demo = CS.Demo
-- 允许访问私有成员
-- 热更新替换
xlua.private_accessible(Demo)
xlua.hotfix(Demo,"Update",function(self)
if(Input.GetKeyDown(CS.UnityEngine.KeyCode.Space)) then
local temp1 = GameObject.Instantiate(goodsit,grid.transform):GetComponent(typeof(UI.Image))
temp1.sprite = pc1
temp1:GetComponent(typeof(UI.Button)).onClick:AddListener(function ()
info:SetActive(true)
info.transform:GetChild(1):GetComponent(typeof(UI.Text)).text = "新生成的temp1"
end)
end
-- 热更添加逻辑代码 使用新添加的资源
if(Input.GetKeyDown(CS.UnityEngine.KeyCode.W)) then
-- 生成Cube 和 Sphere
GameObject.Instantiate(Cube)
GameObject.Instantiate(Sphere)
end
end)
这个案例只是一个简单的入门Xlua,还需自己进行精进和探索