AssetBundle和Resources资源加载的区别
对比维度
Resources
AssetBundle (AB 包)
资源打包方式
自动打包,放在Assets/Resources目录下即被包含
手动打包,需通过脚本或编辑器指定资源打包
是否包含在主包
是,所有资源打包进游戏安装包
否,独立于主安装包,需单独分发
动态更新支持
不支持,修改需重新发布整个安装包
支持,可通过网络更新单个 AB 包
加载方式
Resources.Load(资源名称),无需路径细节
先加载 AB 包,再通过bundle.LoadAsset(资源名)
卸载控制
只能通过Resources.UnloadUnusedAssets()批量卸载
支持AssetBundle.Unload()单独卸载某个包
安装包体积影响
增大初始安装包体积
不影响主包体积,资源可按需下载
内存效率
整体优化,可能占用更多内存
按需加载,内存占用更可控
分平台支持
不支持分平台单独打包
支持为不同平台生成优化后的 AB 包
适用场景
小型项目、静态资源、原型开发
中大型项目、需更新资源、按场景划分的资源
复杂度
简单,无需额外管理逻辑
复杂,需处理打包、版本控制、下载逻辑
AssetBundle 在 Unity 中,AssetBundle 是一种打包格式,用于将游戏资源(如模型、纹理、音频、预制体等)在运行时动态加载。它是优化游戏资源管理和减小安装包体积的核心技术,尤其适用于大型游戏、DLC(下载内容)和资源热更新。在使用AssetBundle进行打包时,不能将C#进行打包,因为C#是编译性语言,需要进行编译。
AB包资源加载
加载AB包的流程,首先从加载AB包,在加载AB包中的资源,实例化加载的AB包资源,管理资源的生命周期。
加载AB包可以 分为同步加载与异步加载资源
同步加载
1 2 3 4 5 6 7 8 9 AssetBundle ab = AssetBundle.LoadFromFile(Application.); AssetBundle ab = AssetBundle.LoadFromFile(Application.streamingAssetsPath+"/" +"model" ); Gameobject prefab = ab.LoadAsset<GameObject>("prefabName" ) Instantiate(prefab);
异步加载
1 2 3 4 5 6 7 8 9 10 11 12 13 private IEnumerator LoadAsset (string name,string path ) { AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path); yield return request; AssetBundle ab = request.assetBundle; AssetBundleRequest assetRequest = ab.LoadAssetAsync<GameObject>(name); yield return assetRequest; GameObject prefab = assetRequest.asset as GameObject; Instantiate(prefab); }
AssetBundle.LoadFromFileAsync(path) 返回的是一个 AssetBundleCreateRequest 对象,而不是直接返回 AssetBundle。这个 AssetBundleCreateRequest 对象在异步加载完成后,才会包含真正的 AssetBundle 实例,所以需要在异步加载完成后,通过 request.assetBundle 获取到已经加载好的 AB 包。
异步加载时,LoadFromFileAsync 只是启动了加载过程,等 yield return request 之后,request.assetBundle 才是真正 的 AB 包对象。这和同步加载(直接返回 AB 包)不同,异步加载需要等加载完成后再取出结果
依赖包 关于AB包的依赖: 一个资源身上用到了别的AB包中的资源这个时候如果只加载自己的AB包通过它创建对象会出现资源丢失的情况这种时候需要把依赖包一起加载了才能正常。加载主包,在得到主包中固定文件,从固定文件中得到依赖信息。只能知道一个包依赖那几个包,但是不会知道包中的资源依赖那几个包,一次性全部加载进来
1 2 3 4 5 6 7 8 9 10 AssetBundle abmain = AssetBundle.LoadFromFile(Application.streamingAssetPath + "/" + "PC" ); AssetBundleManifest abManifest = abMain.LoadAsset<AssetBundleManifest>("AssetBundleManifest" ); string [] strs = abManifest.GetAllDependencies("model" );for (int i = 0 ;i < strs.Length;i++){ AssetBundle.LoadFromFile(Application.streamingAssetPath + "/" + strs[i]); }
AB包资源加载管理器 使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GameObject prefab = ABManager.Instance.LoadRes<GameObject>("characters" , "player" ); ABManager.Instance.LoadResAsync("weapons" , "sword" , (Object obj) => { if (obj != null ) { Instantiate(obj); } }); ABManager.Instance.LoadResAsync<Texture>("ui" , "icon" , (Texture tex) => { if (tex != null ) { GetComponent<Image>().texture = tex; } });
管理器 提供一个加载AB包中资源的管理器
3个同步加载函数以及3个异步加载的函数,分别传入需要加载的AB包以及AB包中需要加载的资源,同时加载该Ab包的依赖。window平台应该是PC包先加载
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 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Events;public class ABManager : MonoBehaviour { private static ABManager _instance; public static ABManager Instance { get { if (_instance == null ) { GameObject obj = new GameObject("ABManager" ); _instance = obj.AddComponent<ABManager>(); DontDestroyOnLoad(obj); } return _instance; } } private AssetBundle mainAb = null ; private AssetBundleManifest manifest = null ; private Dictionary<string , AssetBundle> abDic = new Dictionary<string , AssetBundle>(); public string PathURL { get { return Application.streamingAssetsPath + "/" ; } } private string MainABName { get { #if UNITY_IOS return "IOS" ; #elif UNITY_ANDROID return "Android" ; #else return "PC" ; #endif } } public void LoadAB (string abName ) { if (string .IsNullOrEmpty(abName)) return ; if (mainAb == null ) { mainAb = AssetBundle.LoadFromFile(PathURL + MainABName); if (mainAb == null ) { Debug.LogError("Failed to load main AssetBundle" ); return ; } manifest = mainAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest" ); } string [] dependencies = manifest.GetAllDependencies(abName); foreach (string dependency in dependencies) { if (!abDic.ContainsKey(dependency)) { AssetBundle depAb = AssetBundle.LoadFromFile(PathURL + dependency); if (depAb != null ) { abDic.Add(dependency, depAb); } else { Debug.LogError($"Failed to load dependency: {dependency} " ); } } } if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathURL + abName); if (ab != null ) { abDic.Add(abName, ab); } else { Debug.LogError($"Failed to load AssetBundle: {abName} " ); } } } public Object LoadRes (string abName, string resName ) { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { Object obj = ab.LoadAsset(resName); if (obj is GameObject) return Instantiate(obj); return obj; } return null ; } public Object LoadRes (string abName, string resName, System.Type type ) { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { Object obj = ab.LoadAsset(resName, type); if (obj is GameObject) return Instantiate(obj); return obj; } return null ; } public T LoadRes <T >(string abName, string resName ) where T : Object { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { T obj = ab.LoadAsset<T>(resName); if (obj is GameObject) return Instantiate(obj) as T; return obj; } return null ; } 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 ) { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { AssetBundleRequest request = ab.LoadAssetAsync(resName); yield return request; if (request.asset is GameObject) callback?.Invoke(Instantiate(request.asset)); else callback?.Invoke(request.asset); } else { callback?.Invoke(null ); } } 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 ) { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { AssetBundleRequest request = ab.LoadAssetAsync(resName, type); yield return request; if (request.asset is GameObject) callback?.Invoke(Instantiate(request.asset)); else callback?.Invoke(request.asset); } else { callback?.Invoke(null ); } } 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 { LoadAB(abName); if (abDic.TryGetValue(abName, out AssetBundle ab)) { AssetBundleRequest request = ab.LoadAssetAsync<T>(resName); yield return request; T asset = request.asset as T; if (asset is GameObject) callback?.Invoke(Instantiate(asset) as T); else callback?.Invoke(asset); } else { callback?.Invoke(null ); } } public void Unload (string abName, bool unloadAllLoadedObjects = false ) { if (abDic.TryGetValue(abName, out AssetBundle ab)) { ab.Unload(unloadAllLoadedObjects); abDic.Remove(abName); } } public void ClearAll () { foreach (var ab in abDic.Values) { ab.Unload(false ); } abDic.Clear(); if (mainAb != null ) { mainAb.Unload(false ); mainAb = null ; manifest = 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 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 using System.Collections;using System.Collections.Generic;using UnityEngine;using UnityEngine.Events;public class ABManager : Singleton <ABManager >{ private AssetBundle mainAb = null ; private AssetBundleManifest manifest = null ; private Dictionary<string , AssetBundle> abDic = new Dictionary<string , AssetBundle>(); public string PathURL { get { return (Application.streamingAssetsPath + "/" ); } } private string mainAbname { get { #if UNITY_IOS return "IOS" ; #elif UNITY_ANDROID return "Android; #else return " PC"; #endif } } //加载AB包 public void LoadAB(string abName) { if (mainAb == null) { mainAb = AssetBundle.LoadFromFile(PathURL + mainAbname); //加载完主包 manifest = mainAb.LoadAsset<AssetBundleManifest>(" AssetBundleManifest");//加载完固文件 } //加载目标包的依赖包资源 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); } } //加载目标包,如果没有加载过在加载 if (!abDic.ContainsKey(abName)) { AssetBundle ab = AssetBundle.LoadFromFile(PathURL + abName); abDic.Add(abName, ab); } } /// <summary> /// 从AB包加载资源 /// </summary> /// <param name=" abName">目标资源所在的 AssetBundle 包名称</param> /// <param name=" resName">要加载的资源在AB包内的名称</param> //同步加载 public Object LoadRes(string abName, string resName) { LoadAB(abName); Object obj = abDic[abName].LoadAsset(resName); if (obj is GameObject) return Instantiate(obj); else return obj; } //同步加载,根据Type类型 public Object LoadRes(string abName, string resName, System.Type type)//使用时加上typeof(GameObject) { LoadAB(abName); Object obj = abDic[abName].LoadAsset(resName, type); if (obj is GameObject) return Instantiate(obj); else return obj; } //同步加载,根据泛型加载 public T LoadRes<T>(string abName, string resName) where T : Object { LoadAB(abName); T obj = abDic[abName].LoadAsset<T>(resName); if (obj is GameObject) return Instantiate(obj); else return obj; } //此处的异步加载是资源加载,而非AB包异步加载 //根据名字异步加载 public void LoadResAsync(string abName, string resName, UnityAction<Object> callback) { StartCoroutine(ReallyLoadResAsync(abName, resName, callback)); } public IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction<Object> callback) { LoadAB(abName); AssetBundleRequest assetRequest = abDic[abName].LoadAssetAsync(resName);//异步加载资源 yield return assetRequest; if (assetRequest is GameObject) callback(Instantiate(assetRequest.asset)); else callback (assetRequest.asset); } //根据Type异步加载 public void LoadResAsync(string abName, string resName, System.Type type,UnityAction<Object> callback) { StartCoroutine(ReallyLoadResAsync(abName, resName, type, callback)); } public IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction<Object> callback) { LoadAB(abName); AssetBundleRequest assetRequest = abDic[abName].LoadAssetAsync(resName, type);//异步加载资源 yield return assetRequest; if (assetRequest is GameObject) callback (Instantiate(assetRequest.asset)); else callback(assetRequest.asset); } //根据泛型异步加载 public void LoadResAsync<T>(string abName, string resName, UnityAction<T> callback) where T : Object { StartCoroutine(ReallyLoadResAsync<T>(abName, resName, callback)); } public IEnumerator ReallyLoadResAsync<T>(string abName, string resName, UnityAction<T> callback) where T : Object { LoadAB(abName); AssetBundleRequest assetRequest = abDic[abName].LoadAssetAsync<T>(resName);//异步加载资源 yield return assetRequest; if (assetRequest is GameObject) callback( Instantiate(assetRequest.asset) as T); else callback(assetRequest.asset as T); } public void UnLoad(string abName) { if (abDic.ContainsKey(abName)) { abDic[abName].Unload(false); //只删除AB包,不删除资源 abDic.Remove(abName); } } public void clearAb() { AssetBundle.UnloadAllAssetBundles(false); abDic.Clear(); mainAb = null; manifest = null; } }
重新回顾xlua框架 xLua 是腾讯开源的一款用于 Unity 的 C#/Lua 热更新框架 。它允许开发者在 C# 中调用 Lua 代码,也支持在 Lua 中调用 C# 的功能,从而实现无需重新编译和发布应用 就能更新游戏逻辑的能力
最重要的就是LuaEnv(lua虚拟机)
1 2 using xlua;LuaEnv luaenv = new LuaEnv();
管理 Lua 虚拟机的生命周期
加载和执行 Lua 脚本
管理 Lua 内存
在使用中,我创建一个通用的类来快捷的在C#调用lua脚本,首先是创建一个lua虚拟机。其次我们在其他脚本中调用的时候需要初始化,因此创建一个初始化的函数,同时实现文件重定向,在文件重定向的时候,存在2种情况,一种是lua脚本存在于一个文件夹中,我们将所有的lua脚本都存在这个文件夹中,在重定向函数中指明存储的路径。另外一种是AB中,在测试的时候将lua打包进ab’包,再从ab中调用,但是我们需要实现AB包加载器,先加载AB包后即可使用
LuaManager.cs
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 using System;using System.Collections;using System.Collections.Generic;using System.IO;using UnityEngine;using XLua;public class LuaManager : Singleton <LuaManager >{ public LuaEnv luaEnv; public LuaTable Global { get { return luaEnv.Global; } } public void Init () { if (luaEnv != null ) return ; luaEnv = new LuaEnv(); luaEnv.AddLoader(MyCustomLoader); } public void DoluaFile (string name ) { string str = string .Format("require('{0}')" ,name); DoString(str); } private byte [] MyCustomLoader (ref string filepath ) { string path = Application.dataPath + "/Lua/" + filepath + ".lua" ; if (File.Exists(path)) { return File.ReadAllBytes(path); } return null ; } private byte [] MyCustomABLoader (ref string filepath ) { #region 没有AB包管理器,加载AB包 #endregion #region 从AB包管理器加载 TextAsset txt = ABManager.Instance.LoadRes<TextAsset>("lua" , filepath + ".lua" ); #endregion if (txt == null ) { return null ; } return txt.bytes; } public void DoString (string str ) { if (luaEnv == null ) return ; luaEnv.DoString(str); } public void Tick () { luaEnv.Tick(); } public void Dispose () { luaEnv.Dispose(); luaEnv = null ; } }
如何在C#中获取lua的变量以及函数 使用luaManager中的Global,也就是lua中的大G表,因为在lua中,所有的全局变量都存储在大G表中,因此我们需要通过获取大G表来获取到lua中变量和函数LuaManager.Instance.Global,
下面我给出测试案例以及lua边的脚本以及运行结果。需要确保测试的脚本继承monobehaviour以被挂载
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 using System;using UnityEngine;using XLua;public class Main : MonoBehaviour { void Start () { LuaManager.Instance.Init(); LuaManager.Instance.DoluaFile("Test" ); int i = LuaManager.Instance.Global.Get<int >("testNumber" ); bool j = LuaManager.Instance.Global.Get<bool >("testBool" ); string h = LuaManager.Instance.Global.Get<string >("testString" ); LuaFunction luaFunc = LuaManager.Instance.Global.Get<LuaFunction>("testfunction" ); luaFunc.Call(); LuaFunction luaFunc1 = LuaManager.Instance.Global.Get<LuaFunction>("testfunction3" ); luaFunc1.Call(1 , "hello" , true ); Action simpleFunc = LuaManager.Instance.Global.Get<Action>("testfunction" ); Func<int , int > addFunc = LuaManager.Instance.Global.Get<Func<int , int >>("testfunction1" ); addFunc(1 ); if (simpleFunc != null ) simpleFunc(); print(i); print(j); print(h); } void Update () { } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 print ("Test" )testNumber = 1 testBool = true testString = '!2' testfunction = function () print ("无返回值无参数" ) end testfunction1 = function (a) print ('有参数有返回值' ) return a end testfunction2 = function () print ("多返回值" ) return 1 ,2 ,3 ,false ,"1" end testfunction3 = function (a,...) print ("变长参数" ) print (a) obj = {...} for k,v in pairs (obj) do print (k,v) end end
映射到 C# List(数组型 table) Lua 数组通常从 1 开始 ;C# List 索引从 0 开始
映射时按 1..n 依次取值
1 2 3 4 5 nums = { 10 , 20 , 30 } players = { { id=1 , name="A" }, { id=2 , name="B" } }
C# 侧映射要点
table[1] → list[0]
List<Player>:每个元素仍是 table,要递归映射到类
** 常见坑**
table “数组部分”中间有 nil:比如 {1, nil, 3},Lua 的长度 #t 行为会不稳定(不同实现/版本可能不同)。 实战建议:不要依赖 #t ,而是用明确长度字段或用遍历取最大整数 key。
映射到 C# Dictionary<K,V>(字典型 table) Lua table 的 key 可以是多种类型:string / number / boolean / table / function…
C# Dictionary 需要固定 K 类型 → 最常用是 Dictionary<string, T> 或 Dictionary<int, T>
1 2 3 4 5 6 cfg = { hp=100 , mp=50 , name="hero" } items = { [1001 ] = { name="sword" , atk=10 }, [1002 ] = { name="shield" , def=5 }, }
映射要点
string key → Dictionary<string, object/T>
int key → Dictionary<int, object/T>
value 是 table → 递归映射到类/字典/列表
常见坑
混合 key:有 string 又有 int → 你得选一种策略:
拆分成 List + Dictionary
统一成 Dictionary<string, object>(把数字 key 转成字符串)
用 Dictionary<object, object>(不推荐:类型处理麻烦,性能/序列化也差)
Lua基础语法 全局变量 在lua语言中,默认创建的变量就是全局变量。全局变量不需要声明,给一个变量赋值,该变量就是全局变量。默认访问一个全局变量也不会报错,但是会输出nil,如果想要删除全局变量,直接给全局变量赋值为nil
1 2 3 print (b) b = 10 b = nil
数据类型 Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。
Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。
数据类型
描述
nil
这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean
包含两个值:false和true。
number
表示双精度类型的实浮点数
string
字符串由一对双引号或单引号来表示
function
由 C 或 Lua 编写的函数
userdata
表示任意存储在变量中的C数据结构
thread
表示执行的独立线路,用于执行协同程序
table
Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。
可以使用type关键字来回去变量的数据类型
1 2 print (type (nil ))print (type ('123' ))
nil nil 类型表示一种没有任何有效值,它只有一个值 – nil,例如打印一个没有赋值的变量,便会输出一个 nil 值,对于全局变量和 table,nil 还有一个”删除”作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉。而在bool类型中,nil 等同于false,都代表假
1 2 3 4 5 6 b = 10 b = nil tab1 = {key1 = 'value1' ,key2 = 'value2' } print (tab1.key1)tab1.key1 = nil print (tab1.key1)
bool bool数据类型只有false和nil为假,其他都为真,包括负数
1 2 3 4 5 if -1 then print ("true" ) else print ("false" ) end
string 在lua中,字符串由一对双引号或单引号来表示,也可以用 2 个方括号 “[[]]” 来表示”一块”字符串。在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字,在lua里面字符串的拼接不是使用 +,而是使用..进行拼接。
1 2 3 4 5 6 7 print ("string1" )print ('string1' ) html = [[stfafaifhahu]] print (html) print ("2" +"2" ) print ("2+2" ) print ("2" +2 )
table 在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。也可以在表里添加一些数据,直接初始化表,也可以直接给表进行赋值。不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认初始索引一般以 1 开始。table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fruits = {"apple" , "banana" , "cherry" } print (fruits[2 ])person = {name = "Bob" , age = 30 , city = "New York" } print (person.name) person["gender" ] = "男" for key, value in pairs (person) do print (key, value) end a = {1 ,"avc" , {},function ,end } a [5] = 123table.insert = (a,2,"d") local s = table .remove (a,2 )
运算符 在lua中,没用自增运算符和复合运算符,不支持三目运算符和位运算符
6.1 算术运算符 1 2 3 4 5 6 7 8 a = 12 b = 5 print (a + b)print (a - b)print (a * b)print (a / b)print (a % b)print (a ^ b)
6.2 关系运算符 1 2 3 4 5 6 7 8 9 m = 8 n = 8 print (m == n)print (m ~= n)print (m > n)print (m < n)print (m >= n)print (m <= n)
6.3 逻辑运算符 1 2 3 4 5 p = true q = false print (p and q)print (p or q)print (not p)
条件分支语句 Lua 编程语言流程控制语句通过程序设定一个或多个条件语句来设定。在条件为 true 时执行指定程序代码,在条件为 false 时执行其他指定代码。控制结构的条件表达式结果可以是任何值,Lua认为false和nil为假,true和非nil为真。在lua中没有switch条件分支
要注意的是Lua中 0 为 true,在lua中elseif之间没有空格
语句
描述
if 语句
if 语句 由一个布尔表达式作为条件判断,其后紧跟其他语句组成。
if…else 语句
if 语句 可以与 else 语句 搭配使用, 在 if 条件表达式为 false 时执行 else 语句代码。
if 嵌套语句
你可以在if 或 else if 中使用一个或多个 if 或 else if 语句 。
1 2 3 4 5 6 7 8 9 10 11 12 score = 78 if score >= 90 then print ("Grade: A" ) elseif score >= 80 then print ("Grade: B" ) elseif score >= 70 then print ("Grade: C" ) elseif score >= 60 then print ("Grade: D" ) else print ("Grade: F" ) end
循环语句 Lua 语言提供了以下几种循环处理方式:
循环类型
描述
while 循环
在条件为 true 时,让程序重复地执行某些语句。执行语句前会先检查条件是否为 true。
for 循环
重复执行指定语句,重复次数可在 for 语句中控制。
repeat…until
重复执行循环,直到 指定的条件为真时为止
循环嵌套
可以在循环内嵌套一个或多个循环语句(while do … end;for … do … end;repeat … until;)
循环控制语句 循环控制语句用于控制程序的流程, 以实现程序的各种结构方式。
Lua 支持以下循环控制语句:
while 循环 1 2 3 4 5 6 count = 1 while count <= 5 do print (count) count = count + 1 end
for 循环 1 2 3 4 5 6 7 8 9 10 11 for i = 1 , 10 , 2 do print (i) i = 1 end numbers = {10 , 20 , 30 , 40 } for index, value in ipairs (numbers) do print (index, value) end
repeat - until 循环 1 2 3 4 5 num = 1 repeat print (num) num = num + 1 until num > 5
函数 因为lua是解释性语言,执行的顺序是从上到下逐条执行的,必须在函数的定义之后才能够使用函数。在有参函数中,因为是不指定参入的参数的类型是,所以支持所有的基本类型传入,如果传入函数的参数与函数的参数个数不匹配,不会报错,只会补空nil或者是丢弃 ,在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 function multiply (x, y) return x * y end product = multiply(4 , 7 ) print (product)function get_name_age () return "Alice" , 25 end name, age = get_name_age() print (name, age)function F (...) arg ={...} for i,#arg do print (arg [i]) end end F(1 ,2 ,3 ,4 ,5 ,6 )
lua函数闭包
1 2 3 4 5 6 7 8 function f1 (x) return function (y) return x+y end end f = f1(10 ) print (f(5 ))
函数f1返回一个匿名函数,该匿名函数捕获了外部变量x 也就是10,写成闭包。f变量接受一个返回值为函数的赋值,所以f = function(y),变量f成为一个函数。当调用f(5),闭包内的x与传入的y(值为 5)相加,输出结果 15。正常f1的变量在生命周期接受后就被释放,在此次通过闭包的方式,延迟了声明周期。
数组 Lua 中并没有专门的数组类型,而是使用一种被称为 “table” 的数据结构来实现数组的功能。Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。在 Lua 索引值是以 1 为起始,但你也可以指定 0 开始。通过#来计算数组的长度,在计算数组的长度时,只要遇到nil,则不计算nil及以后的数组元素,只计算之前的。
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 a = {1 ,2 ,nil ,1 ,3 ,nil } array = {11 , 22 , 33 , 44 , 55 } print (array[3 ])for i = 1 , #array do print (array[i]) end a = {{1 ,2 ,3 },{2 ,3 ,4 }} for i = 1 ,#a do b = a[i] for j =1 ,#b do print (b[j]) end end a = {[0 ]=1 ,2 ,3 ,[-1 ]=4 ,5 } print (a[0 ])print (a[-1 ])print (#a)
表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fruits = {"apple" , "banana" , "cherry" } print (fruits[2 ])person = {name = "Bob" , age = 30 , city = "New York" } print (person.name) person["gender" ] = "男" for key, value in pairs (person) do print (key, value) end a = {1 ,"avc" , {},function ,end } a [5] = 123table.insert = (a,2,"d") local s = table .remove (a,2 )
特殊的表结构,字典。字典是由键值对得到的
1 2 3 4 5 6 7 8 9 10 11 a ={["name" ] = "111" ,["age" ]=13 } print (a["name" ])print (a.name)a["Sex" ] = false a["Sex" ] = nil
在lua中,可以使用表来模拟面向对象,在调用方法时,存在两种方式,通过.进行调用或者是通过通过:进行调用。通过冒号进行调用,会默认把调用者作为第一个参数传入方法中。同时,冒号也可以用来声明函数,如果是冒号声明,相当于有一个默认参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Student ={ name = "111" , Sex = false , Learn = function () print (Student.Sex) print ("11111111" ) end } Student.name = "222" Student.age = 18 Student.speak = function () print ("I say" ) end Student:Learn()
多脚本执行 Lua提供了一个名为require的函数用来加载模块。要加载一个模块,只需要简单地调用就可以了。例如:
或者
执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。并且执行require后,可以使用其他模块中的全局变量以及方法,但是局部变量无法使用,如果使用返回nil
1 2 3 4 5 6 7 8 new_variable = 100 print (_G .new_variable) _G ["dynamic_variable" ] = 200 print (dynamic_variable)
迭代器遍历 ipairs遍历,遍历的索引依旧是从下标1开始遍历,小于等于0的索引遍历不到。并且只能找到连续索引的键,如果中间断续了,它也无法遍历出后面的内容。
1 2 3 4 a ={[0 ]=1 ,2 ,[-1 ]=3 ,4 ,5 ,[5 ]=6 } for i,k in ipairs (a) do print ("ipairs遍历键值" ..i.."_" ..k) end
pairs迭代器遍历能够把所有的键都找到,同时也可以只遍历键
1 2 3 4 a ={[0 ]=1 ,2 ,[-1 ]=3 ,4 ,5 ,[5 ]=6 } for i,k in pairs (a) do print ("pairs遍历键值" ..i.."_" ..k) end
协程 lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。协同程序可以理解为一种特殊的线程,可以暂停和恢复其执行,从而允许非抢占式的多任务处理。
方法
描述
coroutine.create()
创建 coroutine,返回 coroutine, 参数是一个函数,当和 resume 配合使用的时候就唤醒函数调用
coroutine.resume()
重启 coroutine,和 create 配合使用
coroutine.yield()
挂起 coroutine,将 coroutine 设置为挂起状态,这个和 resume 配合使用能有很多有用的效果
coroutine.status()
查看 coroutine 的状态 注:coroutine 的状态有三种:dead,suspended,running,具体什么时候有这样的状态请参考下面的程序
coroutine.wrap()
创建 coroutine,返回一个函数,一旦你调用这个函数,就进入 coroutine,和 create 功能重复
coroutine.running()
返回正在跑的 coroutine,一个 coroutine 就是一个线程,当使用running的时候,就是返回一个 coroutine 的线程号
元表 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。例如,使用元表我们可以定义Lua如何计算两个table的相加操作当 Lua 试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。 __add 等即时字段,其对应的值(往往是一个函数或是 table)就是”元方法”。
setmetatable(table,metatable): 对指定 table 设置元表(metatable),如果元表(metatable)中存在 __metatable 键值,setmetatable 会失败。
getmetatable(table): 返回对象的元表(metatable)。
**rawget(table,变量):**只会在table中寻找该元素,不会在元表查找变量
**rawset(table,变量,变量值):**会忽略newindex设置,只会在自己的表中修改变量的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 meta = { __tostring = function () return 'pap' end } tab1 = {} setmetatable (tab1,meta)print (tab1)meta = { __tostring = function () return 'pap' end , __call = function (a,b) print (a) print (b) print ('__call被调用' ) end } tab2 = {} setmetatable (tab2,meta)tab2(1 )
同时,可以使用元表进行运算符进行重载,如果实现2张表进行相加,会寻找元表是否存在 __ add函数,存在则进行相加,减法也是如此,寻找元表中是否存在 __sub, _ _index当子表中找不到某一个属性时,会到元表中 _ _index指定的表中去寻找索引,建议将 _ _index写在函数外部,因为写在解释性语言边执行边解释,然后在内部因为表还没有完全初始化,所以为nil。如果在meta表中index也找不到这个元素,在meta表的元表的index去寻找
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 metafather ={ } metafather.__index = {age = 2 } meta = { __add = function (a,b) return a.a + b.a end , __sub = function (a,b) return a.a - b.a end , __mul = function (a,b) return a.a * b.a end , __div = function (a,b) return a.a / b.a end , } setmetatable (meta,metafather)meta.__index = meta tab1 = {a= 10 } tab2 = {a= 10 } setmetatable (tab1,meta)print (tab1+tab2)print (tab1-tab2)print (tab1*tab2)print (tab1/tab2)print (tab1.age)
面向对象 封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 object = {} object.id = 1 function object :Test () print (self .id) end function object:func () local obj = {} setmetatable (obj,self ) self .__index = self return obj end local obj = object:func()print (obj.id)obj:Test() obj.id =2 print (object.id) obj:Test()
继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 object = {} object.id = 1 function object:func () local obj = {} setmetatable (obj,self ) self .__index = self return obj end function object:subClass (ClassName) _G [ClassName] = {} local obj = _G [ClassName] setmetatable (obj,self ) self .__index = self return obj end object:subClass("Person" ) local p1 = object.func(Person) print (p1.id)
多态 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 object = {} object.id = 1 function object:new () local obj = {} setmetatable (obj,self ) self .__index = self return obj end function object:makeSound () print ("object makeSound" ) end function object:subclass (className) _G [className] = {} local child = _G [className] setmetatable (child,self ) self .__index = self self .base = self return child end object:subclass("childobject1" ) object:subclass("childobject2" ) function childobject1:makeSound () print ("childobject1 makeSound" ) end function childobject2:makeSound () print ("childobject2 makeSound" ) end childobject1.makeSound() childobject2.makeSound() local ob = object:new()print (ob.id)
XLua框架 Unity游戏引擎的Lua热更新框架,允许开发者使用Lua脚本来实现游戏逻辑的热更新。
核心功能
Lua 与 C# 互调
支持 C# 调用 Lua
支持 Lua 调用 C#
自动生成绑定代码
热更新能力
无需重新发布游戏客户端
通过下载 Lua 脚本更新游戏逻辑
性能优化
Lua解析器 Lua解析器是将Lua源代码转换为可执行代码的核心组件,它负责词法分析、语法分析和代码生成等关键步骤
Lua解析器通常包含以下几个主要模块:
词法分析器(Lexer) :将源代码分解为标记(tokens)
语法分析器(Parser) :根据语法规则构建抽象语法树(AST)
代码生成器(Code Generator) :将AST转换为字节码或机器码
符号表管理 :跟踪变量和作用域
Lua文件加载重定向 Lua文件加载重定向是指在Lua模块加载过程中,通过自定义的加载逻辑改变原始文件的加载路径或内容。这种机制允许开发者灵活控制Lua脚本的加载来源和方式。通过xlua自带的AddLoader方法用于向Lua环境添加自定义的模块加载器,允许你控制Lua脚本的加载方式和来源。AddLoader接受一个自定义加载器的方法。获得文件的路径和内容。
requires(“Lua脚本”) 首先通过自定义函数寻找文件,在从默认路径寻找。
大概流程如下,如果使用 AddLoader方法时,传入的自定义加载器方法返回 nil(未找到指定路径的Lua文件),XLua 会继续尝试在默认路径(即 Resources文件夹及其子目录)下查找对应的 Lua 文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 using System.IO;using XLua;using UnityEngine;class LuaLearn :MonoBehaviour { public void Start () { LuaEnv luaEnv = new LuaEnv(); luaEnv.DoString("require('Main')" ); luaEnv.AddLoader(CustomLoader); } private byte [] CustomLoader (ref string filepath ) { string path = Application.dataPath + "/Lua/" + filepath + ".lua" ; if (File.Exists(path)){ return File.ReadAllBytes(path); } return null ; } }
lua文件加载管理器 核心逻辑流程
初始化 Lua 环境并注册 AB 包加载器
创建LuaEnv实例(Lua 虚拟机)
仅注册 AB 包专用加载器,确保所有 Lua 脚本都从 AB 包加载
从 AB 包加载 Lua 脚本资源
通过 AB 包管理器加载指定 AB 包(如名为lua的包)
从 AB 包中读取 Lua 脚本(以TextAsset形式存储,通常后缀为.lua)
提取TextAsset的字节数据或文本内容
通过DoString执行加载的 Lua 内容
将加载到的 Lua 脚本内容(字符串或字节数组)传入luaEnv.DoString()
执行 Lua 代码,完成逻辑调用
1.从本地资源加载 从Assets/Lua/目录直接读取原始Lua文件。
功能:从本地Assets/Lua目录加载 Lua 脚本
工作流程:
拼接完整文件路径
检查文件是否存在
存在则返回文件字节数组,否则返回 null
1 2 3 4 5 6 7 8 9 10 private byte [] MyCustomLoader (ref string filepath ){ string path = Application.dataPath + "/Lua/" + filepath + ".lua" ; if (File.Exists(path)) { return File.ReadAllBytes(path); } return null ; }
2.从AssetBundle资源加载,从StreamingAssets/lua加载AssetBundle文件
功能:从 AssetBundle 资源包中加载 Lua 脚本
工作流程:
通过 AB 包管理器加载指定的 TextAsset 资源
检查资源是否存在
存在则返回字节数组,否则返回 null
注:Lua 脚本在 AB 包中通常以 txt 后缀存储,避免打包识别问题
1 2 3 4 5 6 7 8 9 10 private byte [] MyCustomABLoader (ref string filepath ){ TextAsset txt = ABManager.Instance.LoadRes<TextAsset>("lua" , filepath + ".lua" ); if (txt == null ) { return null ; } return txt.bytes; }
3.加载完成lua脚本后,执行lua脚本
1 2 3 4 5 6 public void DoString (string str ){ if (luaEnv == null ) return ; luaEnv.DoString(str); }
C#调用lua 全局变量的获取 通过xlua中的 LuaEnv中的global变量的方法来获取全局变量,get获取变量,set设置变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public LuaTable Global { get { return luaEnv.Global; } } LuaTable globalTable = luaEnv.Global; int number = globalTable.Get<int >("testVar" ); luaEnv.Global.Set("testVar" , 456 ); luaEnv.Global.Set("testStr" , "Hello Lua" ); luaEnv.Global.Set("testFlag" , true ); List<int > list = LunEnv.Instance.Global.Get<List<int >>("testList" ); for (int i = 0 ; i < list.Count;i++){ } Dictionary<int ,object > list = LunEnv.Instance.Global.Get<Dictionary<int ,object >>("testList" );
函数的获取
无参无返回值
有参有返回值
多返回值:使用out和ref来实现
变长参数
使用委托来获取函数
1 2 3 4 5 6 7 8 public delegate void Func () ;--方法1 Func func = LuaManager.Instance.Global.Get<Func>("传入lua函数名" ); --方法2 UnityAction ua = LuaManager.Instance.Global.Get<UnityAction>("传入lua函数名" );
1 2 3 4 5 6 7 8 9 LuaFunction addFunc = luaEnv.Global.Get<LuaFunction>("Add" ); object [] result = addFunc.Call(10 , 20 );Debug.Log($"10 + 20 = {result[0 ]} " ); luaEnv.DoString("print(Add(5, 7))" );
类和接口映射table 在 Unity 中使用 XLua 实现 C# 单向调用 Lua 时,类映射和接口映射是两种核心交互方式。类映射通过 [LuaCallCSharp]将 C# 类暴露给 Lua,使 Lua 能直接实例化并操作 C# 对象,适合需要将复杂 C# 对象提供给 Lua 使用的场景。例如定义 [LuaCallCSharp] class Player后,Lua 可通过 CS.Player()创建对象并调用方法。
接口映射则通过 [CSharpCallLua]让 C# 以面向对象方式调用 Lua 逻辑,先在 C# 定义接口如 [CSharpCallLua] interface IEnemy,再在 Lua 中实现对应 Table,最后 C# 通过 Get<IEnemy>()获取实例调用。这种方式比直接使用 LuaTable更安全高效,适合高频调用场景。
两种方式各有所长:类映射侧重将 C# 对象注入 Lua 环境,接口映射则优化 C# 调用 Lua 逻辑的性能和可维护性。实际开发中,类映射常用于提供基础服务,接口映射多用于业务逻辑交互,二者配合可构建清晰的跨语言架构。关键是根据交互频率和复杂度选择合适方式,同时注意通过缓存和委托优化性能。法来实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 PlayerTable = { name = "张三" , level = 25 , skills = {"火球术" , "冰冻术" , "治疗术" }, stats = { attack = 150 , defense = 80 , speed = 120 }, Attack = function (self, target) print (self .name .. "对" .. target .. "发动攻击!" ) return self .stats.attack end , LevelUp = function (self) self .level = self .level + 1 print (self .name .. "升级了! 当前等级:" .. self .level) end }
两种方法得到:一种是通过luatable(效率低,且为引用类型,直接改了lua中的内容),一种是通过global,本质上一样的,因为global是luatable类型
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 using UnityEngine;using XLua;public class LuaTableExample : MonoBehaviour { void Start () { LuaManger.Instance.Init(); LuaManger.Instance.DoluaFile("Main" ); LuaTable playerTable = luaEnv.Global.Get<LuaTable>("PlayerTable" ); string name = playerTable.Get<string >("name" ); int level = playerTable.Get<int >("level" ); Debug.Log($"玩家: {name} , 等级: {level} " ); LuaTable statsTable = playerTable.Get<LuaTable>("stats" ); int attack = statsTable.Get<int >("attack" ); Debug.Log($"攻击力: {attack} " ); LuaTable skillsTable = playerTable.Get<LuaTable>("skills" ); for (int i = 1 ; i <= skillsTable.Length; i++) { string skill = skillsTable.Get<string >(i); Debug.Log($"技能{i} : {skill} " ); } LuaFunction attackFunc = playerTable.Get<LuaFunction>("Attack" ); object [] results = attackFunc.Call(playerTable, "怪物A" ); Debug.Log($"造成伤害: {results[0 ]} " ); LuaFunction levelUpFunc = playerTable.Get<LuaFunction>("LevelUp" ); levelUpFunc.Call(playerTable); playerTable.Set("name" , "李四" ); statsTable.Set("attack" , 180 ); Debug.Log($"修改后的玩家名: {playerTable.Get<string >("name" )} " ); } void OnDestroy () { if (luaEnv != null ) { luaEnv.Dispose(); } } }
知识点
核心内容
考试重点/易混淆点
接口映射基础
通过自定义接口获取Lua表内容,接口中变量需用属性声明
接口不支持成员变量,必须使用属性
特性标记要求
接口前必须添加[CSharpCallLua]特性标记
每次修改接口结构后需重新生成代码
代码生成流程
必须通过XLua菜单执行”生成代码”操作
结构变更时需要先清除旧代码再重新生成
引用拷贝特性
接口是首个引用拷贝类型,修改接口值会同步改变Lua表原始值
与之前学习的值拷贝机制形成对比
属性兼容规则
接口属性与Lua表字段可多可少,多余属性自动忽略
缺失字段返回默认值(如int返回0)
嵌套结构处理
嵌套接口需遵循相同规则(需特性标记+代码生成)
与类映射规则相似但需注意接口特性
调试验证方法
通过值修改测试引用拷贝特性,通过增删属性测试兼容性
需观察控制台输出和Lua表实际变化
lua调用C# 必须先在 C# 中创建并初始化 Lua 虚拟机环境,然后才能执行任何 Lua 代码(包括调用 C# 的 Lua 代码) 。Lua 脚本本身无法”自主”运行,它需要一个宿主程序(Host)来启动 Lua 虚拟机(Lua State)。
类相关
自定义类:使用 CS.命名空间.类名.new()创建实例,通过冒号调用实例方法
U3D自带类:通过 CS.UnityEngine.类名直接访问静态方法或使用构造函数创建引擎对象。
继承Mono类:通过 gameObject:GetComponent("类名")获取组件后调用其公共方法和字段。
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 local obj = CS.UnityEngine.GameObject() obj.name = "Test" GameObject = CS.UnityEngine.GameObject local obj2 = GameObject()local obj3 = GameObject.Find("Test" )Debug = CS.UnityEngine.Debug Debug.Log(obj.transform.position) Vector3 = CS.UnityEngine.Vector3 obj.transform:Translate(Vector3(1 ,0 ,0 )) local t = CS.Test() t:speak("Hello World" ) local t2 = CS.Mr_J.Test2() t2:speak("Hello World" ) local obj4 = GameObject()obj4:AddComponent(typeof(CS.test))
枚举类型 自定义枚举 :通过 CS.命名空间.枚举类型访问,如 CS.MyGame.ItemType。
系统自带枚举 :通过 CS.UnityEngine.枚举类型访问,如 CS.UnityEngine.DayOfWeek
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 GameObject = CS.UnityEngine.GameObject local idleState = CS.MyGame.CharacterState.idleprint ("枚举值:" , idleState)local stateFromNumber = CS.MyGame.CharacterState.__CastFrom(1 )print ("从数字转换:" , stateFromNumber)local stateFromString = CS.MyGame.CharacterState.__CastFrom("atk" )print ("从字符串转换:" , stateFromString)
数组,List,字典 数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 local obj = CS.SSS()print (obj.array.Lenght)for i = 0 ,obj.array.Length - 1 do print (obj.array[i]) end local array = CS.System.Array.CreateInstance(CS.System.Int32, 5 )for i = 0 , 4 do array[i] = i * 10 end for i = 0 , array.Length - 1 do print ("数组元素[" .. i .. "]: " .. array[i]) end local length = array.Length
List
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 local obj = CS.SSS()print (obj.list.Count)local list1 = CS.System.Collections.Generic.List(CS.System.Int32)()list1:Add(10 ) list1:Add(20 ) list1:Add(30 ) for i = 0 , list1.Count - 1 do print ("List元素[" .. i .. "]: " .. list1[i]) end list1:Remove(20 ) list1:Contains(10 ) list1:Clear()
字典
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 local obj = CS.SSS()local dict = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.System.Int32)()dict:Add("apple" , 100 ) dict:Add("banana" , 200 ) dict:Add("orange" , 300 ) local valueif dict:TryGetValue("apple" , value) then print ("apple的价格: " .. value) end print ("banana的价格: " .. dict["banana" ])dict["grape" ] = 400 if dict:ContainsKey("orange" ) then print ("包含orange" ) end local enumerator = dict:GetEnumerator()while enumerator:MoveNext() do local current = enumerator.Current print ("键: " .. current.Key .. ", 值: " .. current.Value) end dict:Remove("banana" ) print ("字典数量: " .. dict.Count)
拓展方法 想要使用C#的拓展方法,必须加上特性
1 2 3 4 5 6 7 8 9 [LuaCallCSharp ] public static class StringExtensions { public static int WordCount (this string str ) { return str.Split(' ' ).Length; } }
因为是静态方法,直接可以通过.调用
1 2 3 local count = CS.StringExtensions.WordCount("hello world" )print (count)
lua使用C#重载函数 在 Lua 中调用 C# 的重载函数时,由于 Lua 只有 number 一种数值类型(同时涵盖整数和浮点数),而 C# 的重载函数通常会根据参数的精确类型(如 int、float、double 等)来区分,这就可能导致 Lua 无法正确匹配到预期的重载函数,从而引发数据错误或调用异常。
避免参数类型重载 :对于需要在 Lua 中调用的 C# 函数,尽量不使用仅靠数值类型区分的重载,而是使用不同的函数名。
类型显式转换 :在 Lua 调用时,通过中间层或绑定工具提供的机制显式指定参数类型,帮助匹配正确的重载函数。
使用包装函数 :在 C# 端创建一个统一入口的函数,接收参数后根据实际情况转发到对应的重载函数,避免 Lua 直接处理类型重载。
1 2 3 4 local obj = CS.L()print (obj:calc())print (obj:calc(15 ,1 ))
lua使用C#的委托和事件 委托
C# 委托本质是函数指针的包装,在 Lua 中通常需要:
将 Lua 函数转换为 C# 可识别的委托实例
或直接调用已存在的 C# 委托
1 2 3 4 5 6 7 8 9 10 11 12 13 local obj = CS.TestClass()local fun = function (a,b) print ("lua被调用" ,a,b) end obj.myDelegate = fun obj.myDelegate(51 ,"from lua" ) obj:InvokeDelegate(function (a,b) print ("委托参数回调" ,a,b) end )
1 2 3 4 5 6 7 8 9 10 11 12 13 public delegate void MyDelegate (int a, string b ) ;public class TestClass { public MyDelegate myDelegate; public void InvokeDelegate (MyDelegate del ) { del?.Invoke(123 , "from C#" ); } }
事件
C# 事件是特殊的委托(带访问控制的委托成员),在 Lua 中通常通过 += 订阅、-= 取消订阅,但需要注意:
订阅时需保存 Lua 函数的引用,否则可能被 GC 回收
不同框架可能有特定的事件绑定语法(如 AddListener/RemoveListener)
1 2 3 4 5 6 7 8 9 public class EventTest { public event Action<string > OnMessage; public void TriggerEvent (string msg ) { OnMessage?.Invoke(msg); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local eventObj = CS.EventTest()local onMessageFunc = function (msg) print ("收到事件:" , msg) end eventObj.OnMessage = eventObj.OnMessage + onMessageFunc eventObj:TriggerEvent("Hello Lua!" ) eventObj.OnMessage = eventObj.OnMessage - onMessageFunc eventObj:TriggerEvent("测试" )
类型匹配 :
Lua 函数的参数数量和类型需与 C# 委托 / 事件的签名一致(Lua 会自动进行基础类型转换)。
复杂类型(如自定义类)需确保已在绑定框架中注册。
内存管理 :
Lua 函数绑定到 C# 委托 / 事件后,需手动取消订阅,否则可能导致 C# 对象无法被 GC 回收(内存泄漏)。
部分框架(如 ILRuntime)需要显式管理委托的生命周期。
框架差异 :
XLua :支持 +=/-= 语法直接操作事件,自动处理类型转换。
ILRuntime :需通过 Add/Remove 方法操作委托,且需要注册委托转换器。
LuaInterface :通常使用 Delegate.CreateDelegate 手动创建委托实例。
性能考量 :
频繁的委托 / 事件绑定可能有性能开销,建议在初始化时完成绑定,避免运行时频繁操作。
lua调用c#二维数组 在 Lua 中调用 C# 的二维数组需要注意两者数据结构的差异,Lua 中没有原生的二维数组概念,通常用嵌套表(table)来模拟。不同的 Lua-C# 绑定框架(如 XLua、ILRuntime 等)处理方式略有不同,但核心思路是通过绑定层实现二维数组与 Lua 嵌套表的转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class ArrayTest { public int [,] int2DArray = new int [2 , 3 ] { { 1 , 2 , 3 }, { 4 , 5 , 6 } }; public float [,] GetFloat2DArray() { return new float [2 , 2 ] { { 1.1f , 2.2f }, { 3.3f , 4.4f } }; } }
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 local testObj = CS.ArrayTest()local intArray = testObj.int2DArraylocal rowCount = intArray:GetLength(0 ) local colCount = intArray:GetLength(1 ) for i = 0 , rowCount - 1 do for j = 0 , colCount - 1 do local value2 = intArray:GetValue(i, j) print (string .format ("intArray[%d,%d] = %d" , i, j, value2)) end end local floatArray = testObj:GetFloat2DArray()local floatRow = floatArray:GetLength(0 ) local floatCol = floatArray:GetLength(1 ) for i = 0 , floatRow - 1 do for j = 0 , floatCol - 1 do print (string .format ("floatArray[%d,%d] = %.1f" , i, j, floatArray[i, j])) end end
维度索引 :
C# 二维数组的索引从 0 开始,与 Lua 表的默认索引一致。
必须通过 GetLength(dimension) 获取维度长度,不能直接用 # 运算符(Lua 的 # 对 C# 数组可能返回不正确的结果)。
数组类型差异 :
上述示例针对 C# 的多维数组(int[,]),如果是 交错数组(int[][],数组的数组),处理方式不同:
交错数组在 Lua 中会被转换为嵌套表,可直接用 [i][j] 访问(如 array[0][1])。
可通过 # 获取外层数组长度(如 #array 获取行数)
lua调用C#的null和nil比较
C# 的 null :表示引用类型未指向任何对象(引用为空),仅适用于引用类型(如类实例、字符串、数组等),值类型(如 int、float)不能为 null(除非使用可空值类型 int? 等)。
Lua 的 nil :表示变量未定义或没有有效值,是 Lua 中唯一的 “空” 值,可用于任何变量。
在 Lua 调用 C# 时,绑定框架通常会做如下映射:
C# 中的 null 传递到 Lua 中,会被转换为 Lua 的 nil。
Lua 中的 nil 传递到 C# 中,会被转换为 C# 的 null(仅对引用类型有效)。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class NullTest { public string GetNullString () { return null ; } public void CheckNull (object obj ) { if (obj == null ) { Debug.Log("C# 接收:obj 是 null" ); } else { Debug.Log("C# 接收:obj 不是 null" ); } } }
1 2 3 4 5 6 7 8 local test = CS.NullTest()local str = test:GetNullString()print (str == nil ) test:CheckNull(nil )
lua使用C#协程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 util = require ("xlua.util" ) GameObject = CS.UnityEngine.GameObject WaitForSeconds = CS.UnityEngine.WaitForSeconds local obj = GameObject("Coroutine" ) local mono = obj:AddComponent(typeof(CS.test)) local function countingCoroutine () local a = 1 while true do coroutine .yield (WaitForSeconds(1 )) print ("计数:" , a) a = a + 1 end end local coroutineFunc = util.cs_generator(countingCoroutine) mono:StartCoroutine(coroutineFunc)
lua使用C#泛型函数 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 using System;using System.Collections.Generic;public class GenericClass <T >{ private T _value; public GenericClass (T value ) { _value = value ; } public T GetValue () { return _value; } public void SetValue (T newValue ) { _value = newValue; } public TResult Combine <TResult >(Func<T, TResult> converter ) { return converter(_value); } } public class GenericMethodHolder { public static T GetDefault <T >() { return default (T); } public static T Max <T >(T a, T b ) where T : IComparable<T> { return a.CompareTo(b) >= 0 ? a : b; } public static List <T > CreateList <T >(params T[] items ) { return new List<T>(items); } }
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 local intObj = CS.GenericClass(123 ) local strObj = CS.GenericClass("hello" ) print (intObj:GetValue()) print (strObj:GetValue()) intObj:SetValue(456 ) print (intObj:GetValue()) local numStr = intObj:Combine(CS.System.Func_int_string( function (num) return CS.System.String.Format("Number: {0}" , num) end )) print (numStr) local defaultInt = CS.GenericMethodHolder.GetDefault(CS.System.Int32) local defaultStr = CS.GenericMethodHolder.GetDefault(CS.System.String) print (defaultInt) print (defaultStr == nil ) local maxInt = CS.GenericMethodHolder.Max(10 , 20 ) local maxStr = CS.GenericMethodHolder.Max("apple" , "banana" ) print (maxInt) print (maxStr) local intList = CS.GenericMethodHolder.CreateList(1 , 2 , 3 , 4 ) print ("List count: " .. intList.Count) for i = 0 , intList.Count - 1 do print ("Item " .. i .. ": " .. intList[i]) end
xlua背包实践 ILRuntime YooAsset HybridClr 热更新流程
发现Bug并且修改Bug
资源打Ab包上传,代码使用热更方案上传
玩家启动游戏检测是否需要更新
下载补丁包
检测是否更新成功(例如通过MD5校验)
进入游戏
AOT AOT是一种编译方式,也称为提前编译或静态编译。在AOT编译中,源代码在程序运行之前就被编译成机器码。这种编译方式的好处是,由于编译过程在程序运行前已经完成,因此程序在运行时可以更快地启动,并且不需要占用额外的运行时资源来进行编痒。然而,AOT编译的一个主要缺点是,由于它无法接触到程序运行时的信息,因此可能无法生成最优化的代码。
JIT
JIT,即Just-In-TimeCompilation(即时编译),是一种在程序运行时,将中间代码(如字节码)转换为特定平台的机器码并执行的技术,这种技术允许程序在运行时根据需要生成和执行代码,从而提高程序的灵活性和性能。
然而,iOS系统禁止了JIT的编译方式。iOS之所以禁止JIT,主要是出于安全和稳定性的考虑。具体来说,实现JIT的一个基本要求是平台允许修改内存页面的访问权限,使得进程可以执行自己的数据段。但IOS系统封存了内存的可执行权限,变相地封锁了IT编译方式,即机器码被禁止映射到内存。这样的设计可以防止应用程序动态生成和执行任意代码,从而降低被恶意代码攻击的风险。
RunTime Mono IL2CPP