1. 为什么要用脚本语言
为了说明为什么要用脚本语言,首先的知道什么是脚本语言。一般而言,脚本语言是指大部分的动态语言如Javascript,Python,Lua等语言,一方面它们不需要编译直接解释执行,另一方面它们是弱数据类型,一个var类型可以定义所有不同数据类型的变量。而像C,C++,Java,C#这类需要经过编译后执行机器代码或者字节码的强数据类型语言一般不会算作脚本语言,它们一般被定义成很高级很强大的静态语言。当然,这种定义也不是绝对的,甚至你可以自定义一组语法,然后自己写一个简单的解析器,完成某个功能的配置解析执行,我认为这也能算一种脚本语言,只是语言非常简单而已。
对于使用脚本语言要解决什么样的问题,是跟脚本语言的特性息息相关的,对于游戏开发来说,一般会分成几个层面来进行开发。第一个层面是游戏引擎层,这个层面要解决很多密集运算处理和对内存要求超高的管理,如场景渲染,场景管理,裁剪剔除等。所以在这个层面上考虑到性能和内存,C++是唯一人选非他莫属,实际上几乎所有的大型游戏引擎都是用C++写的。游戏引擎层主要解决的是大部分游戏逻辑不相关的部分,这部分对于游戏开发来说,几乎是最稳定的变化不大的部分,而对于具体业务层如果继续使用C++进行开发的话,会让开发效率变的低下。第一点是因为对于大型项目来说,C++语言的编译构建速度是相当之慢,对于经常需要改动进行测试的游戏项目尤其是进行到后期的项目来说,如果因为一个Bug需要重新打包出版本,结果整个团队又要再等半个小时才能测试,这种效率是完全不能容忍的。第二点是C++的开发难度相较于其他语言来说更难,对程序员的硬性要求更高,对于一个游戏团队来说成本就会变的更高。所以一般开发游戏具体业务逻辑时,不会选用C++开发而会选用脚本语言来开发,这就是为什么要用脚本语言的原因。
如果你接触过UE4引擎的话就会发现,如果使用C++编写功能,任何改动都需要重新进行编译,随着逻辑代码量的增加,编译耗时会越来越长,而使用蓝图编写功能的话,几乎改动后都是秒编,效率相当之高,所以使用C++编写游戏逻辑确实会使开发效率降低。
对于Unity游戏引擎而言,它的底层是C++编写的,但是他选择了C#作为脚本语言来开发游戏逻辑功能,所以我说上面对于脚本语言的定义并不是绝对的。C#确实是一门优秀的语言,各种高级语言特性该有的都有,而且编译速度奇快,对于开发PC游戏或者主机游戏我认为全部游戏功能就用C#开发就足够了不用再需要用其他语言了。而对于手机游戏来说,尤其是对于国内的氪金手游来说,热更新是一个非常大的需求,但是Apple平台又禁止了C#的DLL热更新功能,所以全部都用C#开发的方式就行不通了,最后大家又搞了一套Lua语言来作为C#的脚本语言,所以说我认为Unity里面选择使用Lua脚本语言进行游戏开发的原因只有一种,那就是为了热更新。
混合语言的游戏开发系统架构这篇文章写的很好,不仅分了游戏引擎层和逻辑业务层,还独立出来一个工具层,基本上说的就是现在的Unity手机游戏开发框架,引擎底层是C++,工具和编辑器层是C#,游戏逻辑层大家都选的是Lua,这可是2008年时的文章啊,那时Unity引擎是啥样都还不知道。我觉得这对于我如何在Unity上构建一个优秀的游戏开发框架有很大的帮助。
2. 用脚本语言有什么优缺点
说完为什么用脚本语言后,我们需要来聊聊脚本语言有什么优缺点了,正所谓知己知彼才能百战不殆,所以知道脚本语言的优缺点后,我们才能在项目开发过程中设计出合理的构架来使用脚本语言,发挥它的优点规避它的缺点。因为几乎所有的Unity项目都选用的Lua作为脚本语言,所以这里的分析也就以Lua为主了。
Lua脚本语言的优点:
(1)能够进行热更新 在Unity使用Lua脚本进行逻辑开发最大的优点就是能够动态热更新游戏逻辑。对于手机应用商店,一般新提交的应用更新可能至少需要两三天才能够审核完成通过,一方面对于已经正式上线运营的游戏如果发现Bug,那么快速解决问题并且推送给用户就是非常重要的了,否则用户很可能就会快速流失,另一方面对于需要经常添加新功能留住玩家的游戏来说,快速热更新也是相当重要的,尤其是国内的网游,经常更新各种各样的活动玩法提升付费率和留存率是家常便饭,所以非常依赖于热更新功能。当然我认为这不是绝对的,如果一个游戏本身就是精品游戏,那么用户是有耐心等待的,就像《部落冲突》或《炉石传说》每隔一段时间会下载整包更新一样。
实际上,我认为严重依赖于热更新打补丁的流程不一定是个好流程,如果在一个闭合的流程里本身就有个非常严格的测试流程,那么只要稍微有点影响游戏体验的Bug本身就不应该出现在上线的游戏中,更别说那些特别严重的Bug,而且这种流程会无形的给开发团队输送一种思维习惯就是,反正可以热更新修复,所以不用太苛刻于我的代码质量,只要没有重大Crash问题或内存泄露问题就行了,这样反而没有把工程质量放到一直关心的位置上。
(2)强大的动态table数据类型
Lua的table非常的强大,能够灵活地描述各种不同类型数据,表在 lua 中真的是无处不在,首先它可以作为字典和数组来用;此外它还可以被用于设置闭包环境、模块;甚至可以用来模拟对象和类。而且表是无固定长度,可以根据自己的需求随时扩容。
t={}
t[{}] = "table" -- key 可以是表
t[1] = "int" -- key 可以是整数
t[1.1] = "double" -- key 可以是小数
t[function () end] = "function" -- key 可以是函数
t[true] = "Boolean" -- key 可以是布尔值
t["abc"] = "String" -- key 可以是字符串
t[io.stdout] = "userdata" -- key 可以是userdata
t[coroutine.create(function () end)] = "Thread" -- key可以是thread
因为table的强大功能,所有使用lua来描述游戏中的配置数据是相当的方便和灵活,从这个角度思考,Lua天生对配置友好,我们可以不用把游戏的Excel配置数据导出成json或者xml格式,然后在引擎中专门写一套加载逻辑来读写想要的数据,而是我们可以直接把所有Excel配置数据导出成lua的表,然后直接在lua逻辑代码里面读写表,相当的方便。
(3)语言简洁优雅,语法简单
Lua语言是一门很小巧非常轻量级的语言,它的学习难度和学习成本非常的低,一个稍微有点编程经验的人熟悉一个星期就可以完全上手做事了,所以Lua的招人成本也比较低,不像招聘C++程序员,至少需要拥有2~3年的编程经验。
Lua脚本语言的缺点:
(1) 明明在编译期就能解决的错误,为什么需要推迟到运行期才发现
因为编译器不能做语法编译检查,所以编写代码时不能一气呵成,需要小段代码小段代码的进行测试。缺少强大的IDE支持会让开发效率很低,在Visual Studio里用C#开发时,编辑器能够智能补全,使用成员符号时编辑器能够列出当前能够调用的成员变量和函数,可以直接跳转到函数的定义,而使用Lua开发时,因为没有强大的IDE支持,会不停的查找源文件里不同的对象属性的定义,并且由C#端暴露出来的Lua对象更是需要打开C#源码才能查看有哪些成员变量可供使用,导致效率相当底下并且很容易引入运行时错误。
重构很容易出错。静态语言在进行重构时,因为新增加减少或改变的数据类型会在编译期发现语法错误,所以如果不仔细漏掉替换能很快的发现并解决这类问题。而对于弱类型语言,如果进行重构的话,如果不非常仔细的逐一排查很可能会漏掉某个替换,因为动态语言不会编译就不会报错,直至你运行时期才会出现错误,甚至会因为测试不够全面仔细导致将错误隐藏一段时间后才会被发现,那个时候你会一脸懵逼的看着代码完全毫无头绪。所以对于使用动态语言,看似更加方便的弱类型加上解释运行特点,实际上是牺牲了编译器语法自动检查排错为代价的,所以我认为使用动态语言需要进行测试的成本会远远高于使用静态语言。
动态语言的Debug是非常影响效率的一个事情,那些明明能够在编译器就能被检查的错误偏偏要被延后到运行时,我之前用Lua时找一个Bug找了大半天,结果最后发现却是某个函数传递的数据类型是有问题,但对于使用C++或C#就不存在这种问题,编译器就直接报错了。使用Lua真的是经常会遇到这种情况,这样一看,至少我觉得,开发效率反而降低了。
(2) 对Unity编辑器支持不够友好,对跟踪调试支持不够友好
虽然市面上有一些IDE的插件支持调试Lua打断点,例如VS Code下的LuaIDE,还有ArmmyLua等,但是比起强类型语言的断点调试还是要弱很多,甚至我都没有看到有条件断点的功能。并且Unity并不支持对Lua数据的呈现,如果使用C#,你挂上一个Component,直接就可以在编辑器里查看或修改Component上的属性,有时对于错误跟踪或者效果调试非常的方便,而对于使用Lua编写的话就没有办法直接在编辑器里查看某个Lua对象上的值了,非常的不方便。
(3) 成熟的第三方工具或库比较少
相比于C#或C++这类语言,至少我了解在成熟的第三方库上,他们要比Lua要多很多,这也是使用Lua进行开发的一个缺点吧。
3. 如何合理的使用lua来开发游戏
如何合理的使用Lua来开发游戏项目,我认为是一个很复杂的问题,既要能够展现出Lua的优点,也要规避Lua的缺点。目前我接触的最多的就是开发模式就是全部游戏业务逻辑都使用Lua脚本语言进行开发,底层一些资源管理相关的会使用C#进行开发,这种模式简单,直接,暴力,任何业务逻辑都可以做热更新。对于小型游戏项目这种方式到还好,对于大型游戏项目,我认为Lua是不适合用来构架大型应用程序的,不仅仅是Lua,任何弱类型脚本语言都不适合,因为这种类型的语言阅读起来太困难了,你在阅读别人的接口的时候,只有一个参数名,没有参数类型的相关信息,如果不加强接口注释,系统稍微大一点,你就不知道系统各个模块之间到底是怎样关联起来的了。
合理区分变化最大与变化不大的部分,我认为才是合理的使用Lua来。比如UI系统,这快可能是游戏中变化最大最勤的部分,那么这款就完全使用Lua。比如AI系统,我认为没有必要全部使用Lua,对于底层的有限状态机系统或者行为树系统,可以使用C#来实现,然后具体的状态切换逻辑或者条件逻辑可交由Lua来编写,这样不仅可以利用C#与编辑器交互的特性构建可视化的一些功能,也能够最大限度的热更AI的业务逻辑,又比如寻路系统,完全没有必要放在Lua端,要么实现在C#层,要么实现在效率更高的C层,这样能最大化的提高运行效率。
总之,我认为游戏开发中全部使用Lua来开发游戏业务逻辑不是一种优雅的方案。