10 Mar 2007

The Nebula3 Scripting System

Nebula2's scripting system implemented a script interface to C++ classes, where script commands mapped directly to C++ methods. From a technological view point this was a neat concept, but in the end, the Nebula2 scripting system was too low-level and fine grained for the people a scripting system is mainly targeted at: the level designers who need to script game logic and behaviour.

Level logic scripting usually happens on a much higher level then C++ class interfaces. Directly mapping script commands to C++ methods may introduce a dangerous complexity on the scripting level. Bugs are even more likely then in similar C++ code, because scripting languages usually lack strong typing and most "compile time" error checks, so that bugs which normally show up during compilation in C++ often only show up at runtime when using scripting (however this varies between scripting languages). This was an experience we learned from Project Nomads, which was scripted using the Nebula2 scripting system.

So the lesson learned was: do your scripting on the right abstraction level, and: mirroring your C++ interfaces in a scripting language is kind of pointless, because then you're much better off with doing the stuff directly in C++ in the first place.

Instead, the new Nebula3 scripting philosophy is to provide the level designers with a set of (mostly application-specific) building blocks on the "right abstraction level". Of course "the right abstraction level" is difficult to define, because a balance must be met between flexibility and ease-of-use (for instance, should a "Pickup" command move the character into pickup-range, or not?).

Apart from being too low-level, the Nebula2 scripting system also had some technical issues:
  • C++ methods had to adhere to scripting conventions (only simple data types for parameters allowed)
  • A hassle for the programmer. Every C++ method that was scriptable needed additional script interface code (several lines per method).
  • Only nRoot-derived classes scriptable.
  • Object-persistency hardwired to the scripting system (neat concept at first, but the additional dependency made refactoring very hard).
Here's how the Nebula3 low-level scripting system looks like:
  • the base of the scripting system is the Scripting::Command class
  • a Scripting::Command is completely scripting language independent, and consists of a command name, a set of input parameters and a set of output parameters
  • a new script command is created by deriving a new subclass of Scripting::Command, the script command functionality is coded into the OnExecute() method of the Command subclass in C++
  • script command objects must be registered with the ScriptServer singleton before use
  • the ScriptServer is the only scripting-language-specific class in the Scripting subsystem, it will register Command objects as new script command, and translate command parameters to and from the scripting language's C-API
This concept is much simpler then Nebula2's and - most importantly - it isn't interwoven with the rest of Nebula3. It is even possible to compile Nebula3 without scripting support at all by changing a simple #define.

Of course, writing the C++ code of script commands would still be as boring as in Nebula2. Here's where NIDL comes in. NIDL is the cleverly named "Nebula Interface Definition Language". The idea is to reduce the repetitive work when writing script commands as much as possible by defining a simple XML schema for script commands and compile that XML description into the actual C++ code which implements a subclass of Scripting::Command.

The essential information needed for a script command is:
  • the name of the command
  • types and names of input parameters
  • types and names of output parameters
  • the actual C++ code (most often a single line of code)
Some less essential, but convenient information:
  • a description of what the command does and what each parameter means for an automatic runtime help system
  • a unique FourCC code for more efficient streaming over binary data channels
Most script commands translate into about 7 lines of XML-NIDL-code. The XML files are then compiled into C++ code by a "nidlc" NIDL compiler tool. This preprocessing is fully integrated into VisualStudio, so working with NIDL-files doesn't introduce any more hassle to the programmer.

To reduce file clutter (and resulting compile times), scripting commands are grouped into collections of related commands called a library. A library is represented by a single NIDL-XML-file, and is translated into one C++ header and one C++ source file. Script libraries can be registered at application startup with the script server, so if your application doesn't require or want file access from within scripts, just don't register the IO scripting library. This will also reduce the size of the executable, since the linker will drop the C++ code of the scripting library completely if not referenced.

Finally, Nebula3 drops TCL as its standard scripting language, and adopts LUA for its smaller runtime code size. LUA has become the quasi-standard for game-scripting, so it should be easier to find level designer already fluent in LUA coding.