最新消息:

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

编程 koic_zhzz 206浏览 0评论

做游戏引擎的基本都是 c++,为了性能和开发效率,可以理解。但是脚本里 lua 应用普遍,而脚本语言有很多,为什么选择了 lua 呢?是历史的偶然吗?还是因为 lua 的哪些特性使得在游戏开发中特别方便?


因为 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 beginend 。
  • 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 写个最简单的程序,定义个类:

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

多清爽,简单直白,程序不就应该这样写吗?使用也是直接:

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 里把错误标出来:

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

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 的项目有:

知乎里也能找到很多人再用:

想在自己游戏里引入 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

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

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

扩展阅读:

编辑于 2022-11-10 15:40・IP 属地广东

转载请注明:落伍老站长 » 为什么 lua 在游戏开发中应用普遍?

发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址