The Path to Good Scripting is Relative.

Original Author: Jake Simpson

Scripting Languages are hard to get right. It’s so easy to just implement Lua or stackless python, throw a few bindings at it so the in script state can be read by the game engine, say to the scripters “Be careful” and leave it at that. It’s even easier to build something specific to your game engine and throw that over the wall.

The natural results of a lot of these approaches are very cpu expensive scripts, badly written with lots of stack issues, filled with magic numbers that make them *extremely* tied to specific level geometry, lots of experimentation by the scripters and basically a lot of extra code being run for no really good reason.

In my experience with scripting languages (and I should point out my credentials – I was responsible for the scripting language The Sims 2 sits on), it’s usually less about the *actual* scripting language used – this won’t be a “home grown” vs “off the shelf” blogs post – and more about the feature set that is exposed in scripts that make them either easier to write or harder.

The first thing to understand is that relativity is key. What’s meant by that is that scripts should NEVER allow the scripter to write what is basically trig, or write magic numbers in the script that relate to something that can be changed externally to the script. For example, a script that says “If the NPC is within range A of coordinates X, Y and Z” is so specific to the environment it’s being run on that if that environment changes – someone modifies the level’s terrain for example – as to need re-writing every time something changes. I’m sure we can all agree this is bad news.

A script that says “If the NPC is within range A of Object B’s coordinates X, Y and Z” is marginally better, since the values for X, Y and Z are now coming dynamically from object B, which means if you move Object B in the world, the script doesn’t need to be re-written.

However one of the impacts of this approach is that the scripter still has access, at run time, to absolute coordinates (in fact it doesn’t matter if the coordinates they have access to are absolute, relative or whatever – it’s the fact they have access to ‘real’ numbers that matters). Why would you need this? Why to do trig of course! Wait, what?

Seriously, 95% of the time that real coordinate values are made available inside of a scripting language the scripter WILL end up doing some degree of trig math on them, in order to work out stuff. Now, as I’m sure most script supporting tech guys are already saying – “that’s bad news”. And it is. Doing trig math in a parsed language means a) parsing it in the first place, b) doing range check verification on the values coming in – a divide by 0 at run time is a bad thing for example, but most scripters won’t check for that themselves, c) doing the trig (and range checking the results) and then d) returning the result to the script. This is expensive, time consuming and will often be done in some kind of loop. So what to do to avoid this?

Well, a script that says “If the NPC is within range A of Object B” – where ‘within range’ is an actual engine function that is being called, so the math is being done in native code directly without the scripting system ever seeing any coordinates, then all of a sudden the ability to do expensive trig is removed. If you don’t have access to world coordinates, then there’s very little trig you can do on it!

Basically you’ve made your language relative. Everything that happens suddenly becomes relative to objects in the world, wherever they may be, and your scripting code never needs to know what that is. Want to route to a door to open it? A NPC script with the line  “Route to Object A, in front of, distance X, facing” means the routing system will find Object A and plot a route to infront of the Object, at distance X and your NPC will end up facing Object A. At no point is the script doing any trig, nor is it handling any magic numbers except for passing in distance, which really should be defined by the animation you are expecting to run once the NPC arrives at the door (E.g. reach out and grasp the handle) anyway.

Another aspect of relativity is that now we are off loading functionality into the native engine (where is should be, for speed), we should also be offloading object state too. Part of what makes a scripting language attractive is the idea of holding state in the script and not having to ask the game engine for obscure stuff. While this is nice, there’s a balance to be held in knowing that if the script crashes (and it will – there are ALWAYS edge cases), you can actually recover from that if your object state is held in the game engine. All you do is reset certain elements of state and restart scripting (and hope that the conditions where the script crashed where environmental and the environment has changed sufficiently to avoid it happening again). However, if all your state is held in script, when the script crashes, so does all your state. This is why holding essential state externally in the native engine rather than in script is a good idea, plus that comes with the added bonus that when you need to save game state, it’s all in the C++ implementation of Save – you don’t need to add extra code in the actual script to save out state – it’s already being saved in the native engine since that’s where your essential object state is held anyway.

Of course the downside to this approach is that going back and forth between bindings to get a state variable that is being checked or written to often can also be expensive, but like dereferencing pointers, in any given function you should be doing that once and holding the result in a local variable, and then re-writing the new state back out again once the function is complete.

One last thing that every engine should have is a built in profiler – something that can be run in your nightly process (or whenever anyone has time) that actually watches script durations and keeps track of loops, total duration, individual instruction counts / durations etc. This kind of instrumentation (handled in engine, not in the script) is essential to catching scripts that work in isolation but get out of control in real world situations. Without this, scripting optimisation is effectively guess work.

Good scripting implementation is often less about which language you use and more about the features you expose (or not, as the case may be) and constraints you put on your scripters. The more they get used to this, the more they will come to you for extra functionality to be put In Engine, where expensive functionality really should be.