27 Mar 2007

I have put together a current snapshot of Nebula3: nebula3sdk.zip . Keep in mind that this is all work in progress, and that there's not much to play around with yet (basically only the Foundation Layer, no 3d rendering yet, etc...). But at least you should be able to try out the stuff I'm writing about here. After installation, look for a Nebula3 SDK entry in the start menu and start with the Documentation. Software requirements and compilation instructions can be found there.

10 Mar 2007

If you're a programmer, complex code is your worst enemy. Complex code is impossible to understand, impossible to maintain, and impossible to optimize. One could say that programming is a daily struggle against overboarding complexity which constantly creeps in from all corners in a software project.

Here are a few points how complexity happens and what to do against it, the list is by far not complete, and the solutions are not cookbook-recipes. Your mileage may vary dramatically.

Interface first, code second: This is probably the most important point. When starting to work on a new feature or subsystem, it's tempting to get the most complicated implementation details out of the way first. Resist!!! Instead concentrate on the interface first because that's the only thing that matters to the other programmers which are "honored" to use your creation in the future. Step back, look at the bigger picture, and ask yourself: If I were one of them, what functionality would I expect from the subsystem and what would I want the class interfaces to look like? Repeat this after you designed each class interface: "If I would need to work with this class, would I be happy with its interface or not?". Maybe let your fellow programmers look at your classes. Only when you are completely happy with your class interfaces, start to think about the implementation. The good thing of all this upfront work is, once you have a good class interface, the implementation behind it is usually obvious, and "writes itself".

Solve the problem at hand, not future problems: Often programmers want to "do it right once and for all" and create super-flexible subsystems which solve all types of similar problems which may show up in the future. Forget it, never works. Just accept that you cannot foresee the future. Simply write clean code which solves exactly the one problem you have at the moment. Only care about future problem when they actually show up. Then reuse existing code if it makes sense. Writing reusable code is something completely different then writing code which intends to solve imaginary future problems.

Pocket fluff adds up over time: Some subsystems have a trend to grow complex over time. The reason is that programmers that use the subsystem need to add little tweaks, like adding a few new methods, or making a method virtual, or making a method public or protected. This is almost inevitable, and it would be wrong to completely forbid such tweaks. However, it is very important to not let this get out of control! When a subsystem requires many of such tweaks, then this is an indication that something is wrong with the subsystem's design, and a refactoring may be in order. Don't be afraid to cut back superfluous functionality, or to throw the entire subsystem away and do a clean rewrite with the new requirements in mind. The earlier such a refactoring happens, the better. Also, if you intend to do such tweaks to a subsystem, first check back with the guy who maintains or originally wrote the subsystem so he knows what's going on.

Newbie programmers love complex stuff: Unexperienced programmers have a strong tendency towards overcomplicated solutions. Even worse are unexperienced prodigy programmers, because they are convinced they are good, when in reality they still need 10 years of real-world-experience to be really good. There's no simple solution to this, experience cannot be injected but must be learned over many years.

You'll need 3 versions to get it right: That's because you can plan and design ahead as much as you want, you won't know what you actually wanted to achieve until the result is in daily use. Only then will all the little imperfections, design issues and interface bugs show up. When this happens, don't hesitate to throw away and rewrite as soon as realistically possible (often minor nuisances like a "milestone plan" stand in the way, but fix the problem as soon as possible or it will haunt you until eternity).

Know what's out there (and use it): Unexperienced programmers tend to waste too much time reinventing wheels (and making them square instead of round). Sometimes it actually IS better to reinvent a more specialized wheel, but for all the boring stuff, don't be proud and just use what's already there. Make sure you have a good toolbox of utility classes that simplify your daily work, like various container classes, or a good string class, and know how to use them! If you find yourself writing more lines code with mundane, repetitive stuff then actual feature-related code, then you need a better toolbox of low level utility classes!

Don't rely on UML (too much): UML is good and fine for doing the very first design steps and getting a grasp of a problem. But don't use UML to design your subsystem down to the method and member level. You will most likely end up with terribly complex class interfaces on the source code level. What looks simple as an UML diagram is often still too complex as source code. Remember that your fellow programmers don't use your subsystem by drawing UML diagrams but by hacking lines of code into the machine! In fact, if your subsystem can only be understood by looking at its UML diagram, you already lost.

Know your target audience and their work flows: When working as a programmer on a game project, the "target audience" for a feature may be other programmers, level designers, graphics artists or (most importantly) the gamer. Concentrate your first design efforts on that. How will the audience use your new feature? What will their work flow look like? Sit together with a level designer, graphics guy or tester and let them show you how they work or how they would like something to work. Don't expect them though to tell you exactly what to do. It's your job as a programmer to formalize their (usually completely unrealistic) expectations ;)

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:

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.