找回密码
 立即注册
查看: 458|回复: 5

为什么 lua 在游戏开发中应用普遍?

[复制链接]
发表于 2023-3-23 17:13 | 显示全部楼层 |阅读模式
做游戏引擎的基本都是 c++,为了性能和开发效率,可以理解。但是脚本里 lua 应用普遍,而脚本语言有很多,为什么选择了 lua 呢?是历史的偶然吗?还是因为 lua 的哪些特性使得在游戏开发中特别方便?
发表于 2023-3-23 17:21 | 显示全部楼层
看了眼问题日志,这个问题2020年提的,两年前Lua的颓势还不算特别明显。
如韦易笑大神所说,在新的技术栈出现后,游戏领域中有了更好的选择,Lua就不算那么出色了。
1、Lua被发掘

Lua在游戏行业被发现并广泛应用,有两个游戏功不可没:一是魔兽世界,二是大话西游。
魔兽世界用Lua的范围还比较小,基本集中在客户端UI脚本、客户端插件方面。Lua代码的比例不算高。
而大话西游就很有意思,它的服务器逻辑大量采用Lua编写。在当年还用Dephi写网游的时代(汗),这无疑是非常先进甚至超前的。
这些早期的游戏大神也是通过调查研究,发现了Lua的巨大优势,才选择它的。
2、Lua的优缺点正在发生转化

其实Lua还是那个Lua,甚至还在慢慢变好。但是矛盾总是发展变化的,Lua当年打天下的一些“优点”正在变成“缺点”。
Lua当年凭什么从大学走向全世界?它最大的优点是什么?
Lua最大的特点就是短小精悍,但功能“齐全”。这个“功能齐全”不得不打引号。因为你学了Lua会发现,它的一些高阶语法(比如面向对象、继承等等),是通过对元表的巧妙利用实现的。
这种写法特别巧妙的东西,总是会在日常带来一些额外的心智负担,举个简单的例子:
C#支持一种?问号表达式,用来简化对空引用的判断:
player?.item?.use();
// 语法含义是:如果player为null,或者item为null,就不调用use函数了。理论上Lua也支持,但是是利用or表达式、以及表的特性巧妙实现的:
E = {}
local use = {{{player or E}.item or E}.use
use and use()高手表示很过瘾,新手已经看呆。
得益于Lua的巧妙设计,它真的可以实现出很高级的用法。但是另一方面,维护这种代码确实有那么一点不舒服,长期看对整个游戏项目是不利的。
3、今天Lua可能不再是首选脚本语言

Lua的虚拟机设计是教科书式的,它不仅执行较快,而且可以轻松寄生在宿主系统中,且不会对宿主系统有任何影响。这一点Python就几乎做不到。具体原因涉及到线程、沙盒环境等问题,就不展开说了。
正因为Lua的虚拟机优异、本身体量特别小,性能又不错,所以当年能流行开来,确实是有两把刷子。
而在技术继续进化的今天,这些优势变得不那么明显,例如在Unity开发中,已经有了多种可以直接用C#代码编译后用作热更新的技术,Lua不再是首选了。
但Lua的缺点——在工程化中体现出的一些弊端,变得不能忽视。


延伸阅读

评论区反映我这篇回答结束得太草率,确实有一点。
虽然目前Lua有点走下坡路,但它的影响力和占有率还是在的。如果对Lua有兴趣,我的另一篇回答有更为全面完整的介绍,请移步:
Lua 是怎样一门语言?

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-3-23 17:25 | 显示全部楼层
主要是历史原因,当年并没有其他更好的选择。另外lua自身设计上就为了方便嵌入,接入成本也比较低。
lua本身是个简单的语言,连OOP都需要用metatable来模拟,其实不太适合大型的游戏项目。目前Unity开发中为何lua如此流行,这是因为国内热更新的刚性需求,在过去没有原生C#热更新方案之前,这是唯一选择。不过现在Unity下有了HybridCLR这样的革命性的C#热更新技术,以及官方将来也会内置支持C#热更新,这种趋势正在迅速被扭转,越来越多的团队抛弃lua,回归到原生C#开发。因为工作流更简单,开发更高效,运行更高效,内存占用也更小。
根据目前主流引擎支持的语言来看,除了C++外,C#可能才是大势所趋。在语言排行榜中,有足够的流行度,丰富的语言特性,强大的IDE及配套生态,以及优化上限高,适合游戏这种性能敏感的场合的语言,除了C#别无选择。像Unity、CryEngine、godot这些引擎都官方支持C#开发。
发表于 2023-3-23 17:29 | 显示全部楼层
对这问题我还是比较有发言权的,我见证和参与了游戏领域lua的兴起:
我2011年进入游戏行业就开始用lua,那时手游还没兴起,unity还在页游领域苦苦挣扎。那时我在一个小公司做后台主程,后台写业务逻辑用lua。当时由于客户端缺人,于是我在unity上嵌入了lua(2012年),让后台人员也可以写客户端逻辑,并实现部分代码共享。
2015年我进腾讯,虽说我是后台程序,但因为之前有相关经验,就为客户端组件团队开发一个unity下lua组件,预计开发完第一版后由他们接手。由于本人没啥取名的天赋,用了个代表未知变量的x加上lua作为名字,也就是xLua。后来因为客户端组件团队一直没人力接手,而且xLua的内部使用项目逐渐多了,搞着搞着这就成了我的主要工作了。。。
2017年xLua对外开源,也逐步在公司外流行起来,时至今日8000+ star,也得到诸如《原神》这种有巨大影响力的项目使用。
小体积是“那时”lua的大优势

lua在游戏领域的兴起,其它答主的诸如魔兽世界的使用,云风的推荐等都是因数之一。但我觉得lua在逐步兴起的那时(2015年)有个巨大的优势:代码段占用
那时苹果刚要求应用提交64位版本,unity刚为此做了il2cpp。il2cpp早期占用空间大,而同期iOS应用的允许的代码段却很小(ios7以前版本40M,ios7是60M),去掉引擎本身的占用应用捉襟见肘(我印象中引擎本身就占了20M+)。代码段超了直接不让上架,于是大多数项目对第三方组件代码段占用要求十分严苛。我记得xLua当时为了满足一个大项目要求的准入指标,都深入研究il2cpp的代码生成规则,改代码的写法而让il2cpp输出编译后更小体积的cpp代码。而lua在体积方面十分优秀,100K的大小把高级语言的常用特性都支持了,这大小可能一个完整版本的正则实现都不够。
变化是唯一的不变

体积优势的削弱
ios9以上代码段限制已经放宽到900M,体积小也算一种优势,但已经不关键。
脚本逻辑增多,到了百万行级
脚本越写越多,一些项目已经到了百万行的级别,这时候维护,模块间配合,人员流动这些问题如果叠加lua的动态类型,可能会非常严峻。
小游戏的兴起
相信大家之前都被《羊了个羊》刷屏过吧,后面可能还被它的团队拿巨额奖金扎了心。无论你喜不喜欢玩,但不可否认小游戏逐渐成为一个重要的渠道,越来越多的团队开始重视起来:《越来越多手游在进军微信小游戏》
怎么应对变化

早在xLua开源后不久,我就想做一套基于js的方案,背景是我希望搞一套更合规的脚本方案(苹果下用苹果的jscore,android下用google的v8),但由于种种原因,一直没能搞起。
到了后来要搞unreal的脚本方案,我想着反正都要重来,不如语言也重选,最终选择是基于typescript,于是有了puerts,那时的选型依据可以看看这篇文章:《puerts偿还了xLua哪些技术债》,不过这篇文章已经是两年前的了,无论是游戏领域还是puerts本身都有很多新变化:
小游戏专精装
小游戏的重要性在增强,而puerts在小游戏/h5也有独特的优势:unity下可以实现js的直接运行。而lua是把lua虚拟机编译成wasm,然后在wasm虚拟机上跑。js的一类公民的运行方式和这种虚拟机套虚拟机的运行方式在性能稳定性都更有优势。
小游戏/H5,如果结合puerts本身的unreal/unity同时支持,如果一个公司同时有unreal、unity、小游戏项目,使用puerts可以精简技术栈,也可能实现跨项目共享组件、经验、人员。
nodejs脚本后端支持
这是最新突破,最初puerts只支持v8,后来加了quickjs的支持,然后加了nodejs的pc端,而最近,nodejs支持已经延伸到了手机:《在你的ios、android应用中嵌入官方版nodejs是什么感觉?》
浏览器+nodejs造就了跨平台桌面开发框架王者electron,而游戏引擎+nodejs+pc移动全平台,又能带来什么样的机会呢?
unity跨语言性能数量级增长
我个人并不热衷进行什么性能竞赛,觉得够用就好,但问题是什么样的数据叫“够用”呢?
xLua已经经过诸多项目的验证,姑且把它作为一个“够用”的基准吧,但问题是之前unity下puerts的跨语言比xlua要慢,究竟还够不够用就没法直接给答案了。这块没进展就只能等待项目的验证了。
不过我也没闲着,unreal版本的支持没那么忙我就会探索下unity版本的优化方案。但之前一直没能找到质变的方案,直到几周前!!
我终于找到质变的优化方案,效果十分惊人:最常用的成员方法测试,android有了数量级的提升,从原来的落后于xlua,变成7倍反超!ios下也测得3倍反超。
这个优化目前还没完工,计划在unity puerts的v2版本放出。

-------2013/3/13更新-------
puerts v2已经发布预览版,也有项目已经实装了v2。
关于性能有些补充:v2预览版发布时,和当时最新的xlua发布版本对比,在android下确实有很大的优势,个别测试项甚至有10倍的差距,不过后来发现是ndk r15c的cmake支持有问题,导致xlua在android下很慢(之前其它方案和xlua的性能对比对比的也是这个慢的版本,疑似编译成debug版本了),更新到ndk r21b后性能大幅提升,不过还是比puerts v2要慢。
发表于 2023-3-23 17:32 | 显示全部楼层
主要是早年能给游戏用的内嵌式脚本引擎只有 Lua。
然后,Lua 不仅自带 coroutine 而且还是 Stackful coroutine,可以非常方便地在宿主和脚本之间转移控制流,因此非常适合作为嵌入式脚本用——实际上,支持 Stackful coroutine 的语言全世界都没几个……
发表于 2023-3-23 17:41 | 显示全部楼层
因为 QuickJS 这样的东西没有早出来几年,否则根本没有 Lua 什么事情,归根揭底,Lua 并不是一门好语言:

  • 作用域默认是 global 的,不是 local 的,但凡最近三十年发明的语言,变量和函数定义基本都是默认 local 的作用域 ,lua 这种默认 global 的设计,强迫你到处写满 local,简直是一口气直追 50 年前的古圣先贤。
  • 索引从 1 开始:记忆里只有 Pascal / QBasic 是这么做的,但 pascal 事实上允许索引从任意位置开始(从 0 / 1 / 100 开始都可以)。
  • 到处是 nil,你的代码四处和 nil 作斗争,明明可以有更优雅的机制的,却什么都用 nil。
  • 到现在都没有 unicode 支持,字符串是 bytes 的别名。
  • 到现在都没有 switch/case,只能写几十行 if/else,隔壁 python 都能模式匹配了。
  • 到现在都没有 type hint,隔壁的 python 在 7 年前就有 type hint 和 type check 了。
  • 项目更新速度异常缓慢,最近十年尤其如此,作者以出世的态度做入世的项目。
  • 前几个版本 table 长度好像还要自己数,现在不用了,但至今打印个 table 内容都没标准方法
  • 至少有 5 种方法定义一个模块,3 种方法定义一个类。
  • 缺乏基础库,每个项目都重新发明了一套,导致你的脚本很难跨项目复用。
  • 一个项目里的代码基本很难不做修改的给第二个项目用,知识无法积累。
  • 缺乏妥善的周边工具。
  • 更多的选择意味着更多的迷惑与更多的束缚,选择多看似更好,但往往最终带来更多痛苦。
明明是 90 年代才发明的语言,浑身透着一股 60-70 年代的味道。
那么使用 QuickJS 代替 lua 的有哪些好处呢?

  • QuickJS 同 Lua 一样小巧,代码就几个文件,运行只需要 200KB 的内存就能跑个简单程序。
  • QuickJS 遵从 ES2020 标准,可以跑全部 ES2020 测试用例,也能轻松支持 TypeScript。
  • 基于 JS 的技术栈有丰富的前人成果供你使用,生态更好。
  • JavaScript  的人员很容易招聘。
  • 少打字: { }  vs begin end 。
  • JavaScript 有 Uint8Array, Float32Array 等内建类型,能比 Lua 更高效的处理媒体数据。
  • 简单逻辑直接用 JavaScript 撸,复杂业务可以上优雅又安全的 TypeScript 。
  • 逻辑可以复用到 Web / Electron 上,向 web 迁移容易很多。
  • QuickJS 在众多 JS 虚拟机实现里,性能是比较好的,V8 下来就是它了。
  • Lua 在 github 上只有 1 万个开源项目,很多还是用 Lua 的宿主项目和 neovim 配置,非纯 Lua 项目,你复用不了。
  • JavsScript 在 github 上有 38 万个项目,大部分是可以用被你复用的纯 js 项目。
  • TypeScript 短短几年,在 github 上就有 13 万个项目了。
  • 团队在 JavaScript 上积累的知识可复用到:移动应用,桌面应用和 web 开发,不光做游戏。
  • JS/TS 有很多优秀的开发环境和丰富的周边工具。
事实上周边一些中型引擎最近两年都完成了 QuickJS 的集成,用它逐步替代 Lua,架不住 js 的人好招聘,技术生态好,架不住 js 还可以有 ts 的加持。
所以说,QuickJS 要是早出来几年,根本没 Lua 什么事情了,老项目选 Lua 是没办法,新项目可以多看看,多比较下,没必要看着继续在 Lua 上面继续浪费时间。

--
没有对比就没有伤害,TypeScript 写个最简单的程序,定义个类:


多清爽,简单直白,程序不就应该这样写吗?使用也是直接:
var p = new Person("skywind", 18, 1800)
console.log(p.toString())
Lua 里所谓 less is more,就是不给你提供个 class,告诉你可以用 table + setmetatable 模拟:
Person = {}
Person.__index = Person

function Person:create(name, age, salary)
        local obj = {}
        setmetatable(obj, Person)
        obj.name = name
        obj.age = age
        obj.salary = salary
        return obj
end

function Person:toString()
        return self.name .. ' (' .. self.age .. ') (' .. self.salary .. ')'
end

local p = Person:create('skywind', 18, 1800)
print(p:toString())你摸着良心说说你想写哪种代码?Lua 这种类定义,一眼看过去真的是一团乱麻,这还不算最恶心的,等你开始写继承的时候才恶心,还有那个 : 符号和主流语言格格不入。
这种程序写大了是很容易一团乱麻的,容易写飞,你忘记写一行 Person.__index = Person 你可能怎么死的都不知道,很多新技术最初的目标都在于简化现有技术,但是最终却以增加了更多额外的复杂性收场,其他语言直接定义个 class 这么简单基础的事情,在 lua 里都那么恶心。
有的项目为了简化这件事情,实现了叫做 class 的函数,让你可以这么定义一个类:
Account = class(function(acc,balance)
              acc.balance = balance
           end)

function Account:withdraw(amount)
   self.balance = self.balance - amount
end

-- can create an Account using call notation!
acc = Account(1000)
acc:withdraw(100)好看么?不好看,但它帮你处理好了 setmetatable,继承等等琐事,避免遗漏,可这个项目实现的 class 和别的项目实现的 class 又不兼容,互相不能继承不说,就连实例化都可以有 n 种方法:
p = Person:create("project1", 18, 1800)
p = Person:new("project2", 18, 1800)
p = Person("project3", 18, 1800)项目 1 和项目 2 的实例化函数(构造)名字用的不一样,项目3 自己实现了 class,直接调用类名,第一个项目实现的类,按第二个项目的方法无法调用。
没有标准就无法协同,就连外面的 Editor/IDE 都很难得知你定义了个什么类,或者类里有些什么成员,因此无法在开发时给予你更多的帮助和支持。
写多了你会问自己,为什么要在 class 定义兼容性这种莫名其妙的事情上浪费这么多时间呢?为什么不能像 ts/js 一样所有项目统一用 new 实例化,用 class 关键字声明呢?
这时候 Lua 会告诉你:“这叫做 Less is more 原则,你不懂”,是不是听了很想砸键盘?
正是由于 Lua 的残缺,导致了语言碎片化,每个项目都要在一些很根本的东西上自己搞一套,导致项目之间代码很难复用,导致知识无法积累,大部分 lua 代码很难脱离所在项目,独立存在,知识积累不起来的后果就是 Lua 虽比 TypeScript 早 21 年,开源项目却不到 TypeScript 的 1/10 。
翻过去看看前面的 TypeScript 对比下,谁美谁丑?谁好谁坏?程序写大了谁的更容易维护?一目了然的事情;TypeScript 还能在编译期就帮你在 Editor 里把错误标出来:


vscode 里都不需要编译,边写边实时标注错误,用 Lua 的话,非要运行到那里,你才能得知出错了。这就是 Lua 的 "Less is more" ,不告诉你错哪里了,让你自己运行时自己踩地雷去,最终你花费了更多(more)的时间为它的残缺(less)买单。
所以说 Lua 不适合写大程序,程序大了 Lua 代码容易写散,容易失去维护性。
Lua 有个很迷惑人的话术,叫做:“Lua 的定位从来就是小而精,不是 Python/Java”,你要真的只用 Lua 写一些一两百行的小代码,配置之类的,我也无话可说,但游戏开发里动辄用 Lua 写几万行的新模块,不得不面对复杂性和可维护性时,就不要再用 “小而精的定位” 当成它语法残缺的挡箭牌了。

--
再给你们欣赏下,如果 Lua 想不依赖任何库,怎么检测一个文件是否存在:
-----------------------------------------------------------------------
-- file or directory exists
-----------------------------------------------------------------------
function os.path.exists(name)
        if name == '/' then
                return true
        end
        if os.native and os.native.exists then
                return os.native.exists(name)
        end
        local ok, err, code = os.rename(name, name)
        if not ok then
                if code == 13 or code == 17 then
                        return true
                elseif code == 30 then
                        local f = io.open(name,"r")
                        if f ~= nil then
                                io.close(f)
                                return true
                        end
                elseif name:sub(-1) == '/' and code == 20 and (not windows) then
                        local test = name .. '.'
                        ok, err, code = os.rename(test, test)
                        if code == 16 or code == 13 or code == 22 then
                                return true
                        end
                end
                return false
        end
        return true
end为了保证代码的 portable,不依赖 C 模块,只使用 Lua 标准 api 写出来就是这样,全是 work-around,再来欣赏下如何求一个文件的绝对路径:
-----------------------------------------------------------------------
-- absolute path (system call, can fall back to os.path.absolute)
-----------------------------------------------------------------------
function os.path.abspath(path)
        if path == '' then path = '.' end
        if os.native and os.native.GetFullPathName then
                local test = os.native.GetFullPathName(path)
                if test then return test end
        end
        if windows then
                local script = 'FOR /f "delims=" %%i IN ("%s") DO @echo %%~fi'
                local script = string.format(script, path)
                local script = 'cmd.exe /C ' .. script .. ' 2> nul'
                local output = os.call(script)
                local test = output:gsub('%s$', '')
                if test ~= nil and test ~= '' then
                        return test
                end
        else
                local test = os.path.which('realpath')
                if test ~= nil and test ~= '' then
                        test = os.call('realpath -s \'' .. path .. '\' 2> /dev/null')
                        if test ~= nil and test ~= '' then
                                return test
                        end
                        test = os.call('realpath \'' .. path .. '\' 2> /dev/null')
                        if test ~= nil and test ~= '' then
                                return test
                        end
                end
                local test = os.path.which('perl')
                if test ~= nil and test ~= '' then
                        local s = 'perl -MCwd -e "print Cwd::realpath(\\$ARGV[0])" \'%s\''
                        local s = string.format(s, path)
                        test = os.call(s)
                        if test ~= nil and test ~= '' then
                                return test
                        end
                end
                for _, python in pairs({'python3', 'python2', 'python'}) do
                        local s = 'sys.stdout.write(os.path.abspath(sys.argv[1]))'
                        local s = '-c "import os, sys;' .. s .. '" \'' .. path .. '\''
                        local s = python .. ' ' .. s
                        local test = os.path.which(python)
                        if test ~= nil and test ~= '' then
                                test = os.call(s)
                                if test ~= nil and test ~= '' then
                                        return test
                                end
                        end
                end
        end
        return os.path.absolute(path)
end先尝试用 realpath ,不行再尝试用 perl 和 python 来求绝对路径 ,最后不行再自己根据当前目录重新计算。同时如果有 cffi 的话,会先尝试用 cffi 导出 msvcrt 的 GetFullPathName 来提供加速,都不行的话自己根据当前路径 join 计算。
爽吧?
不建议把时间浪费在 Lua 上,你在 Lua 上积累的三年经验,将来如果不做游戏就全废了,但如果你把经验积累在 js/ts 上,不做游戏你还可以做非常多的领域。
--

--
答疑:
1)Lua 没有 global 只有 env,说 global 不准确。
我用 global 只是个代称,具体指代 _G 还是 _ENV 看你用不用 luajit 或者 5.1,就是 “非 local ” 的意思,不用纠结,默认污染的是 global 可以理解成 “默认污染 _G 或者 _ENV”就行了。
2)Lua 的数组可以从 0 开始计数的。
看这里:韦易笑:别被忽悠了 Lua 数组真的也可以从 0 开始索引?
3)Lua 的优势是虚拟机非常小。
QuickJS 也非常小,代码就几个文件,运行内存只要 200KB,就能跑个简单小程序。
4)JS 也没好到哪里去,对比 Lua 的话。
ES6 标准以前,JS 也许和 Lua 差不多,但 ES6 以后,JS 完善了不少,Lua 已经没法比了;再说,JS 再不好也可以用 ts 加持,QuickJS 支持到 ES2020,轻松跑 ts。
5)Less is more
前面已经说过很多了,Less 可以但不要残缺。

--
目前已知使用 QuickJS 的项目有:

  • Sciter - Multiplatform HTML/CSS/JavaScript UI Engine (好多大厂用它做桌面 UI)。
  • 抖音的图形引擎(去年已经从支持 Lua 变成了同时支持 Lua 和 JS,目前使用 JS 为主)。
  • 支付宝 Cube 项目:支付宝体验科技:Cube 小程序技术详解 | Cube 技术解读
  • miniblink: 龙泉寺扫地僧:使用quickjs替换v8的chromium
  • 北海渲染引擎:北海 (Kraken) v0.9 — 支持 QuickJS 首屏性能再提升 20%
  • 腾讯某工作室,用 QuickJS 做游戏内 UI 系统,多个已上线游戏在用。
  • 腾讯的:PuerTS 为 unity 和 unreal 提供 TypeScript 支持,底层可用 QuickJS 或 V8。
  • Hummer - 移动端高性能跨端开发框架
知乎里也能找到很多人再用:

  • ImWiki:在 Android 使用 QuickJS JavaScript 引擎教程
  • 空童:Windows平台下QuickJS开发及相应C版DLL Module制作简介
  • 空童:Windows和Linux平台动态链接库版QuickJS制作
  • 空童:QuickJS高级玩法—javascript作为脚本嵌入C++

--
想在自己游戏里引入 JavaScript/TypeScript 和 QuickJS 的,不用从头弄,腾讯开源项目 PureTS 已经帮你们做好了,同时为 unity 和 unreal 增加 TypeScript/JavaScript 支持,并且底层可以随意切换 QuickJS 或者 v8:
GitHub - Tencent/puerts: 普洱TS!Write your game with TypeScript in UE or Unity. PuerTS can be pronounced as pu-erh TS


根本无需自己从头搞,可直接用 PureTS 或者参考它自己实现。看这势头,或许不出几年 JS/TS 将逐渐成为游戏项目的标配。

--
扩展阅读:

  • 韦易笑:什么大多数编程语言中的数组都是从0开始计数的?
  • 韦易笑:别被忽悠了 Lua 数组真的也可以从 0 开始索引?


--

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-30 14:13 , Processed in 0.116466 second(s), 28 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表