Nebula3脚本系统

2022-01-11 15:17:21 浏览数 (1)

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关卡设计师更加容易.

0 人点赞