Unity-Lua补充

ooowl
  • 游戏开发
  • unity
  • lua
About 10 min

Unity-Lua补充

反直觉常识

安装不多说去看文档。在IDEA中使用emmylua插件支持lua(settings->Editor->FileType->luaLanguage虽然里面已经添加了一个*.lua.txt但是不管用再加一个一模一样的就管用了)
在文件开头加入#!/usr/local/bin/lua就可以像执行shell一样去执行lua
动态,弱类型语言, 在默认情况下,变量总是认为是全局的。全局变量不需要声明,哪怕是语句块或是函数里,除非用local 显式声明局部变量,否则全是全局的。
在实践中,建议给所有变量加上local,除非你确认一定需要全局变量。不加local所造成的奇怪bug会让你终身难忘,而且local更会快
给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil如果想删除一个全局变量,只需要将变量赋值为nil
只有nil和false是假;0也是真
从1开始的下标(?从其他语言取的时候一定要注意 一般约定,以下划线开头连接遗传大写字母的名字(比如 _VERSION)是Lua内部全局变量的保留字
本身有JIT速度还可以,和其他语言交互比较慢。
lua语法老严格了,最好按照强迫症风格来写,lua是纯解释,被调用的必须要比调用者先声明。 Lua是运行在一个单线程虚拟机上运行所以只有单线程

奇怪的语法

userdata是和其他语言交互的核心,存储任意C的数据结构;table看你存什么,它相当于把数组和字典混合了起来,底层用哈希表还是顺序表看心情(看你存啥他自己转换
对字符串进行算数操作,Lua会尝试类型转换为数字然后操作,转不了就报错(?

a,b,c=1 -- 1,nil,nil 多的就nil
a=1,2,3 -- 1 少了就扔

type(x)==nil -- false 因为返回的是"nil"
type(x)=="nil" -- 这回对了
x~=nil  -- 不等于也很奇葩

"2" + 6=8.0 -- 神奇.
6 .. 8=68 -- 注意返回的是数字 用 .. 的时候必须要有空格

  • t.it[i]本质都是函数调用,类似于getattr
  • table的自然数索引部分会自己扩容一半按2的倍数扩,hash部分每次重哈希消耗比较大
    • array部分和其他语言一样,统计多少,然后扩容+memcpy,但是即使是数组扩容也会把旧表的hash部分重新计算,它有一个重新分配元素是整数索引还是hash索引的过程
    • hash部分和array内存中是分开管理的,只有设置metatable的__eq或超过阈值才会触发rehash,hash会重新计算一遍key插入新表
    • 表在存储的时候连续自然数是放在array部分的,负数,[1, t->asize]外的数都会被存放在hash部分,rehash的时候会重新计算是不是放入array部分合适
    • 建议使用的时候如果知道大小就提前初始化t={nil,nil,nil},如果过于稀疏了array部分甚至会收缩
  • table遍历是按照key的hash来的而不是顺序。
  • table的名称引用之间赋值也是浅拷贝
  • 使用ipairs只会顺着自然数统计数量,截断就停止,使用#和getn也是统计到自然数截断就停止
  • 获取表真的有多少那就老老实实for k,v in pairs(t) do统计(有点笨
  • 当k被删除的时候如果k被table引用作为key是不会被GC的,只是引用没了此时为强引用
    • 给table设置__mode="k"此时如果key被删除了,那么table也会移除这个k-v
    • 有三种模式,k,v,kv看字面意思就懂
    • 当我把key的引用删除本质上是table在检测这个值是不是只有在此table中是最后一个被引用的地方,如果是那就删除
local M={}
funciton M.func1() end -- 这样就可以在外面调用
return M -- 结束
local M = require("moduletest") -- 在另一个文件中获得M调用函数即可

多文件其实和单文件一模一样,非local修饰就对全局空间赋予变量操作, local修饰是表示在当前作用域中是本地的,在函数里面声明就限制在函数里。

  1. 通常在模块开发中,推荐使用 local 表 然后显式返回,这样可以更好地控制作用域
  2. 加载多文件不仅会拿到返回的table,还会在全局package.loaded中注册此module,热更新原理open in new window就是把它置为nil然后重新require替换掉
    • 也会出现循环依赖
    • 这个变量通过环境变量 LUA_CPATH 来初始化没有就用默认的
  3. 公开函数可以访问私有函数,反之不行
  4. 私有函数实际上也可以赋值或者闭包返回,但是不要这么做
  5. 引用了之后没有被local修饰变量就会注册到全局,即便没有导出也会有,如果俩文件声明重名变量,可能会导致变量覆盖
  6. 使用function M:test() end 相当于带了个py中的self进去,而且调用的时候不能混着调用,:定义就:调用点的就点调用。
  7. function M.test(n, ...) local args={...} end 这样就拿到了可变参数,都是按照自然数索引传的

元表,相当于py中的魔术方法,可重载的函数也是有限的。把元表指定给某个表后,相当于为该表添加了特定的重载行为。
两个变量里面都没有发现对应的行为就会报错,行为会优先调用元表的方法没有再调原生,再没有就报错
经典示例两表合并重载加运算符

  • 使用get/setmetatable获取或者设置某table的元表,拿到就可以修改他原先元表的行为
  • 给全局表_G设置metatable重载__newindex __index 可以阻止团队里滥用全局变量
  • 使用rawset和rawget可以绕过元表的index相关方法拿值
local mt={}
mt.__add=function(t1,t2) -- +的重载函数
  local temp={}
  for _,v in pairs(t1)do table.insert(temp,v)end
  for _,v in pairs(t2)do table.insert(temp,v)end
  return temp
end

local t1={1,2,3}
local t2={2}
setmetatable(t1,mt)
local t3=t1+t2

local st="{"
for _,v in pairs(t3)do st=st..v.."," end
st=st.."}"
print(st)
-- {1,2,3,2,}

看一下协程open in new window基本就是yield那一套,可以暂停函数的执行

常用深拷贝

function deepCopy(object)
    local lookup_table = {}  -- 创建一个查找表,用于存储已经复制过的对象,避免循环引用。
    local function _copy(object)
        if type(object) ~= "table" then  -- 如果对象不是表,则直接返回该对象。
            return object
        end
        
        if lookup_table[object] then  -- 如果对象已经在查找表中,则直接返回对应的已复制对象。
            return lookup_table[object]
        end
        
        local new_table = {}  -- 创建一个新的表,并将当前对象与新表的对应关系存入查找表。
        lookup_table[object] = new_table
        
        for key, value in pairs(object) do  -- 遍历原表的每个键值对,递归复制值,并设置到新表中。
            new_table[_copy(key)] = _copy(value)
        end
        
        setmetatable(new_table, getmetatable(object))  -- 设置新表的metatable为原表的metatable,确保元方法等特性也被复制。
        return new_table  -- 返回新创建的表。
    end
    return _copy(object)  -- 调用内部复制函数,开始复制过程。
end

实现简单的面向对象open in new window 看好他的表,利用表的重载方法和metatable, 实现类似于OOP的东西(有点过于灵活了

lua的闭包和其他语言闭包一样,也是函数里面套函数,内部的函数引用的资源内存调用栈变量等都会存放起来,upvalue就是指向引用外部变量的指针,循环upvalue linked list查找,创建的时候标open用完了就置为close,下次GC就删掉。
整个lua就是被包在function main (...)中,所以所有的函数实际上都是闭包,同理函数upvalue一开始就是指向全局栈,当被当作闭包返回才会指向指向upvalue linked list。

lua的GC使用标记-删除法,分代回收新申请的更容易被扫描,lua虚拟机提供了collectgarbage函数手动回收,但是一般不用。GC八股看一下open in new window
但是小心在lua引用的C#对象如果一直不释放就会导致内存泄漏

Warning

热更代码中old_module已经在运行产生的数据变化和变量等不会被影响吗,热更新被替换的函数,万一还有正在运行的不久有问题了吗。我看市面上的解决方法是让玩家退出然后重新进游戏,是不是就是说重新运行游戏相当于lua虚拟机重启了重新加载了一次

gpt
## 为什么游戏要求重新登录?
你的理解完全正确! 重新登录 = Lua虚拟机重启 = 完全重新加载

### 重启的好处:
1. 状态清理 :所有变量、连接、缓存都重新初始化
2. 避免版本混乱 :不会出现新旧代码混合执行的情况
3. 内存清理 :避免内存泄漏和垃圾数据
4. 确保一致性 :所有模块都是同一版本
## 真正的热更新挑战
### 安全的热更新需要:
1. 状态迁移机制 :保存关键数据,更新后恢复
2. 版本兼容性检查 :确保数据结构兼容
3. 执行时机控制 :在安全的时间点进行更新
4. 回滚机制 :更新失败时能够恢复
### 实际应用中的策略:
- 配置热更 :只更新配置数据,相对安全
- 功能热更 :只在特定安全点(如玩家空闲时)进行
- 分模块更新 :只更新独立性强的模块
- 灰度更新 :先在少数玩家上测试
所以你看到的"要求重新登录"确实是最稳妥的方案,避免了热更新的各种风险。真正的无缝热更新技术门槛很高,需要精心设计的架构支持。

lua和C#在交互的时候值类型也可能会因为拆装箱产生GC。 lua中的字符串分长短字符串40个为界,短字符串创建的时候就计算好了hash如果再创建或者调用相同的字符串会指向同一个,长字符串则每次都重新创建并设置GC相关,而且是用到的时候才赋值。

luaGC完了会 整理内存碎片 执行finalizer析构 更新弱引用 不太明白或者不太熟的有 为什么值传递有GCopen in new window
Lua的GC机制open in new window
C#调用Lua细节open in new window
实现简单的面向对象open in new window

踩坑

作为一门副语言够用就行,回头整理遇到的坑



Last Edit: 2025-07-14 12:26:27
Loading...