Nebula2的脚本系统实现了一个面向C++的脚本接口, 它把脚本命令直接映射到了C++方法. 从技术角度来说, 这是一个简捷的思路, 但是对于需要把游戏逻辑和行为脚本化的关卡设计师来说, Nebula2的脚本系统太底层和透明了.
关卡逻辑脚本一般来说构架于比C++接口更高级的层次上, 直接把脚本命令映射到C++方法会把脚本层次弄得错综复杂. Bug甚至会比同样的C++代码更多, 因为脚本语言一般缺少强类型检查和”编译时”的错误检测, 所以在本应在C++编译时发现的Bug会在脚本运行时才发现(这对于不同的脚本语言有所不同). 这是我们从Project Nomads中得出的经验, 它就是用Nebula2的脚本系统驱动的.
所以教训就是: 把你的脚本架构在一个正确的抽象层上, 并且: 把你的C++接口映射到一种脚本语言是没有意义的, 因为那样你不如从一开始直接用C++来做这些东西.
相应的, 新的Nebula3脚本哲学为关卡设计师提供一些在”正确的抽象层”的(大多是限于特定应用)积木. 当然, “正解的抽象层” 很难来定义, 因为这要在灵活性跟易用性之间找到一个平衡( 例如, 一个”Pickup” 命令是不是应该把角色移动到拾取范围内呢? )
除了太底层以外, Nebula2的脚本系统也有一些其它的缺点:
- C++方法必须遵循可以转化为脚本的原则( 只有简单数据类型才可以做为参数 )
- 给程序员带来麻烦. 每个C++方法都需要额外的脚本接口代码( 每个方法几行 )
- 只有派生自nRoot的类可以脚本化
- 对象关联到脚本系统( 思路简单, 但是增加的依赖性会使重构非常困难 )
下面是Nebual3的底层脚本的大概:
- 脚本系统的基础是Script::Command类
- Script::Command是一个完全脚本语言无关的, 它包含了一个命令名称, 一些输入参数的集合还有一些输出参数的集合.
- 一个新的脚本命令通过派生Script::Comand类来创建, 脚本的C++功能代码可以写入子类的OnExecute()方法
- ScriptServer类是脚本系统中仅有一个脚本语言相关的类, 它会把Command对象注册成新的脚本命令, 并且把命令参数在脚本语言和C-API之间做翻译.
这个观念比Nebula2更为简单, 最重要的是, 它不会跟Nebula3的其它部分交织在一起. 甚至可以通过改变一个#define来编译一个没有脚本支持的Nebula3.
当然, 书写脚本命令的C++代码跟Nebula2一样烦人, 这是NIDL的由来. NIDL的是全称是”Nebula Interface Definition Language”. 基本思想是通过为脚本命令定义一个简单的XML schema并把XML描述编译成派生了Script::Command的C++代码, 来尽量减少书写脚本命令的重复性工作.
对于一个脚本命令必不可少的信息有:
- 命令的名称
- 输入参数的类型和名称
- 输出参数的类型和名称
- 对应的C++代码( 通常只有一行 )
还有一些非必须, 但是可以带来便利性的信息:
- 关于命令的作用和每个参数的意义的描述, 这可以作为运行时的帮助系统
- 一个唯一的FourCC(四字符码), 可以更快的通过二进制通道传输
大部分的脚本命令翻译成了大约7行的XML-NIDL代码. 这些XML文件再用”nidlc”NIDL编译器工具编译为C++代码. 这个预处理是VisualStudio完全集成的, 所以使用NIDL文件不会为程序员代来任何困难.
为了减少乱七八糟的文件(编译生成的), 相关的脚本命令被组织到一个叫作库的集合中. 一个库由一个单独的NIDL-XML文件表示, 并且它只会被翻译一个C++头文件和一个C++源代码文件. 脚本库可以在程序启动时注册到ScriptServer, 所以如果你的应用程序不需要脚本访问文件的话, 仅仅不注册IO脚本库就可以了. 这会减小可执行文件的体积, 因为连接器会把没有用到的脚本库丢弃掉.
最后, Nebula3放弃了TCL作为标准的脚本语言, 而采用了运行时代码更加小巧的LUA. LUA已经成为游戏脚本的准规范, 这也使得寻找熟练的LUA关卡设计师更加容易.