Hidden Costs of Scripting Game Behaviour
As a programmer working in the console games industry, an idea I repeatedly encounter is that game behaviour should always be scripted, and that this will automatically lead to various benefits for the game project.
I don’t think expressing game behaviour in scripts is appropriate for all game projects, because the costs will often outweigh the benefits. In fact, I don’t think scripting has been the right choice in any of the game projects with which I’ve been involved. Scripting might be a good choice for large projects, or games in which the variety of behaviours is particularly high compared to other types of content, such as RPGs and dating sims.
In this post I’m going to talk about the costs and benefits of adding scripting to a game project, not all of which are obvious, based on my own experiences of working on game projects. I advocate taking the decision of whether to use scripting on a particular game project with a full appreciation of the costs and benefits.
I’m using “cost” in a broad, project management sense. Most of the costs are paid in programmer time.
Costs and Benefits
The decision to script game behaviour is usually taken early in the project lifecycle, with benefits such as the following in mind:
- Non-programmers will be able to edit the behaviour scripts, freeing up programmers to handle other project tasks.
- There will be a neat, intuitive split between behaviour and other implementation.
- It will be easier to change the sequence of behaviour events.
- Scripting languages provide support for paradigms such as coroutines, allowing for natural, elegant expression of sequences of behaviour events.
These are benefits worth having, but sometimes ignored are the costs of providing all these benefits.
- Structural cost.
- Cost of writing functionality useful for non-programmers.
- Debugging cost.
- Costs associated with the split between scripts and game code. In particular, the choice to ban game state from scripts, and the opposite choice, to allow game state in scripts, each have special costs.
- Cost of creating a flexible behaviour system.
(Another cited benefit of scripting is that behaviour can be changed by editing scripts without needing to recompile the project. I won’t discuss this further, except to note that whether this is a substantial benefit will depend on the cost of recompiling the project. There are usually better ways to speed up compilation than introducing a scripting language.)
I sometimes hear the suggestion that the benefits of scripting will naturally arise as a consequence of paying the structural cost alone. This ignores all the other costs of scripting. In fact, all of the benefits require a great deal of time investment at the start of the project and throughout. I’ve worked on projects in which scripting support was added at an early stage, but time was not budgeted to cover the ongoing costs, so the benefits did not materialise. Because scripting was not made useful for non-programmers, they abandoned it in practice. Scripting ended up as nothing more than a burden on programmers, providing no benefits, wasting programmer time and costing the project money.
Scripting the Behaviour
The usual idea is that game behaviours - things like enemy AI, or sequences of events, or the rules of a puzzle – are expressed in a scripting language such as Lua, probably in a simple top-to-bottom recipe form, without using objects or much branching. Behaviour scripts are often associated with an object instance in game code – expressed in an object-oriented language such as C++ or C# – which does the work. Game systems – things like commentary, physics or menus – and the underlying engine are expressed in object-oriented code without scripting.
Here’s an example. We might want the behaviour script for an enemy guard to look a bit like the following:
int patrol = 0;
while(patrol < 5)
patrol += 1;
The script contains very high-level functions, which will be defined elsewhere in the scripting language; eventually, functions in game code will be called. Our enemy guard also has some state and functionality on the game language side, encapsulated as a specific instance of an EnemyGuard object. The enemy guard script will eventually call functions of the EnemyGuard object. Script bindings are needed to allow this call from the scripting language to the game language.
Data such as the patrol route and spawn points will also affect the behaviour of each specific enemy guard, although not expressed in either the scripting language or the game language. Designers will have tools to create this content. I’m not going to talk about this kind of data in this post.
The game physics system will affect the guard’s movement and generate collision events. Collision events might be handled in game code.
(There are other ways to use scripting languages in game projects. I’ve worked on projects where everything apart from foundational engine code was written using Lua. I’ve also worked on projects where the behaviour of game entities was expressed using object-oriented Lua. I won’t discuss those sorts of usages further in this post, except to briefly note that (for the former case) Lua is slow, and (for the latter case) there are costs but no clear benefits to representing every game entity by a Lua object plus a C++ object.)
Structural costs of scripting are probably the most obvious costs. Let’s list them.
The most obvious cost of scripting is setting up code support for it in the first place. It might be necessary to set up a virtual machine, assign memory, tick it every frame, etc.
Setting up script bindings also has some cost. Many tools exist to automate the generation of script bindings for a code API, but a project decision must still be made about which APIs should be callable from script. Often, some extra effort has to be taken to ensure the API functions are actually useful when called from a script.
The right way to architect script bindings is for the binding function to do nothing other than call a native function. If bindings are written by hand, without automated tools, it’s very common to see functionality start to creep into these binding functions. Educating the project programmers about this has a cost.
Whether using automated binding generation tools or not, there might be a need to add mechanisms to handle object references: each particular enemy guard needs its own separate runtime instance of the enemy guard script, which calls the functions of the correct EnemyGuard object instance.
It’s also necessary to decide on project-specific procedures: how does the game know which script to call when? How do the team know which script relates to which game entity? How do they create new scripts and add them to the game? What should the scripts be called? Non-programmers who will edit scripts may need a different toolchain to programmers. They may not have the tools or skills to rebuild the game project.
Cost of Writing Functionality Useful for Non-Programmers
Allowing non-programmers to edit behaviour scripts frees up programmers to implement other features. Programmer time is often more expensive than designer time, so this can be attractive.
Writing functionality for non-programmers means writing for a different audience, with different skills and concerns. In practice, making it useful for non-programmers to edit behaviour scripts means achieving two goals:
- Making the meaning of script functions clear and appropriately high-level
- Making script functions useful
These goals are often in opposition.
Game code very often has to achieve things which might not seem important to non-programmers, and probably wouldn’t appear in an informal description of the game behaviour, but which are required for the game to work properly: for example, triggering systems such as saving, interactive music and commentary, or implementing detailed collision reactions.
Script functions can be written at a high level to hide this functionality, but this reduces their flexibility, and increases the likelihood that the game won’t work if the order of function calls is changed. If functions are written at too high a level, they won’t be useful to non-programmers. On the other hand, functions written at too low a level may be too hard to understand.
Non-programmers need script functions which allow them to express their ideas. Non-programmers aren’t likely to have programmer levels of skill to understand function implementations, or be comfortable reading language specifications. Designers will probably be willing to learn a few things if it helps them to express their design ideas, but it’s not reasonable to expect them to become programmers.
Functions have to have clear names, and the way to use them has to be intuitive. Achieving this requires programmer time. Simply generating script bindings for existing APIs doesn’t achieve this. Whether project functionality is usable by non-programmers is orthogonal to the question of whether behaviour is expressed in a script.
Tools for debugging scripting languages, if they exist at all, tend not to be up to the standard of proper IDEs such as Visual Studio. There’s a huge jump in productivity when a programmer is able to set breakpoints and inspect variable values. Not having this increases the cost of fixing bugs.
The split between the scripting language and the game language increases debugging cost. It might not be possible to see a callstack which crosses the language boundary, making it necessary to follow by hand.
If the game has state information stored in the scripting side, fixing bugs will often entail debugging on both the scripting side and the game side, following jumps over the language boundary in both directions. I’ve seen projects get into this state, and it’s a nightmare.
An appreciation of the nightmare of fixing bugs on the scripting side and game side simultaneously can lead to the following decree being issued:
Project Decree: Our Scripts Shall Contain No State Information.
Let’s discuss this.
Cost of Banning State from Scripts
This means that scripts will contain no variables which are important to the game state. The game state must all be stored on the game code side, and if the script needs to read or write game state, script bindings must be written to call get/set functions on the game side.
This might mean that the “patrol” variable is banned from our enemy guard script, and we have to rewrite it like this:
We had to write extra functions to get this to work: in fact, we probably had to write new scripting functions, new binding functions and new native game code functions, costing programmer time.
Banning state from scripts means outlawing a lot of the functionality of the scripting language. I’ve already been assuming that more powerful features such as object support aren’t being used in scripts, but banning state goes much further. Scripts become much less powerful and, as the enemy guard example shows, they also become more awkward to write and understand. The benefit of scripting decreases, while the cost simultaneously increases.
Banning the use of language features has its own cost – that of enforcing a particular coding style. Every project worker who edits scripts has to be educated. Realistically, it may well end up being necessary to check over the scripts in the project periodically, since the pressure of project deadlines can easily tempt a stressed worker into bending the rules. The policy has to be actively policed.
Using scripting is sometimes thought to lead naturally to the project being structured in a good way, reducing the burden on senior staff. But, contrarily, the need to actively police the features used in scripts actually increases the burden on senior staff.
On the other hand, taking the opposite policy of allowing state in scripts also introduces special problems.
Cost of Allowing State in Scripts
When we consider enemy guard behaviour, we first think about the mode of active gameplay, with the enemy guard behaving as an independent, autonomous agent. This leads us to model the enemy guard behaviour as an independent script.
However, virtually all games include special lifecycle events which interrupt active gameplay. The player can usually bring up a pause menu at almost any time, and they can usually quit from it. Console games must support other special kinds of pausing such as system messages. Gameplay mechanics can include timers which elapse, smart bombs which affect all enemies, and other “global” events.
When behaviour scripts aren’t allowed to contain state information, it’s likely that the script can just be aborted without notice when special lifecycle events occur, maybe by terminating the virtual machine without notice. But when behaviour scripts are allowed to contain important state information, it’s very likely that that state will have to be cleaned up when special lifecycle events occur. This can mean game code having to call functions on the scripting side. It can also mean that game flow logic has to wait for scripting functions to finish before it can proceed, rather than terminating the script without notice. It leads to a messy situation where, for example, some pause-quit code is on the game side and some is on the scripting side. All this costs programmer time, increases the likelihood of bugs, and makes those bugs more difficult to fix.
Another more subtle problem is that regular gameplay behaviours very often turn out not to be as independent as they first appeared. We might want to ensure that our enemy guards don’t all randomly pick the same idle animation at the same time, or run in different directions. This leads to a need for getters/setters for scripting state, and can lead to complicated call chains from game code to script and back.
This is a particular problem I have with implementing game behaviour on coroutines. Functionality that at first appears independent will be implemented on a coroutine; but very often, later, it turns out not to be entirely independent after all, and the coroutine function has to start checking and setting state information. Coroutines are supposed to provide an elegant way to implement independent sequences of events, replacing ugly state machines in code; but what they evolve into by the end of a game project is often less elegant, and more expensive to debug, than a state machine in code.
It needs to be clear to project workers whether functionality should be implemented in script or in game code. If there isn’t a clear policy that is actively policed, the game can degenerate into a mishmash of script and code, with a high debugging cost but without compensatory benefits. Trying to stick to the letter of the law that all behaviour should be in script – even collision reactions, pause-quit code and so on – will result in very complicated scripts, resembling code, that non-programmers are unable to understand or modify. The more moderate policy that high level behaviour should be in script but low-level details in code will reduce the power and flexibility of scripts, and still requires a clear definition of “high level”. I haven’t come across a simple solution to this problem.
Cost of Creating a Flexible Behaviour System
It’s sometimes said that scripting will make it easier to change the sequence of behaviour events. This is a misconception. Whether the game supports changing the sequence of events is orthogonal to whether game behaviour is expressed in a scripting language.
Let’s take an example. Here’s a script defining an event which takes place in an alligator swamp.
Let’s say we have another script which takes place on the moon.
Can we expect immediately to be able to write a third script like this?
Or a fourth script like this?
It’s very likely that the answer is no. Writing a system which will support the third and fourth scripts, as well as the first two, costs more.
Implementing the first two scripts will have involved writing high-level script functions; possibly writing lower-level script functions; writing or generating script bindings; and writing implementation at various levels in game code. The high-level script functions hide a lot of implementation details.
Alligator objects in game code might get a reference to the Swamp object, and the game will crash or fail to work properly if Alligators don’t have a valid Swamp reference. The Swamp object may only work properly for one round – there might not be any reset code to allow the timer to work again after the end sequence. And so on, and so on.
Here’s the crucial point: it’s very likely that the implementation was written to comply only with the immediate specific design requirements, unless extra time was budgeted to do more. Smart programmers try to anticipate future requirements where possible, but writing systems that can handle extra requirements increases the cost.
Writing an Alligator object that will work without a Swamp object takes implementation time and testing time. Writing an Alligator that reacts appropriately with a Moon object – whatever that means! – takes more implementation time and more testing time. It doesn’t come for free. Writing a system which allows any object to work with any other object or environment costs exponentially more, as does writing a system which allows the sequence of events to be changed. Simply expressing game behaviour in a scripting language rather than C++ doesn’t automatically make the system flexible.
Game projects always have heavy time pressures, and the cost of creating a flexible behaviour system must be justified by project benefits. The fact is that many game projects don’t actually require very flexible behaviour systems: so the project requirements don’t justify the cost. Velocity 2X only had a small number of game entities with different behaviours; most of the variety came from level design. It would have been a poor choice to script the behaviour in that game.
Making useful flexible behaviour systems is expensive. If a particular game project will require a flexible behaviour system, then the cost of implementing it must be considered in the project budget and schedule.
Flexible Systems for Prototyping
Prototyping is often mentioned in discussions about flexible systems. “If we make a flexible system, designers will be able to use it to quickly prototype new design ideas, without requiring programmer time.”
The trouble is that new design ideas are often more than just a rehash of existing functionality. New design ideas very often need new functionality, even at the early prototyping stage: constraining the possibility space to combinations of existing mechanics is often too restrictive to be very useful. Meaningful prototyping needs the ability to try genuinely new things.
I worked on the last PlayStation Buzz! quiz game, “Buzz! The Ultimate Music Quiz”. Every now and then, someone would mention the possibility of making a generic system that would allow the question content, scoring mechanics, animations, visual styles and sound effects of the different rounds to be put together in different combinations, to make new rounds. On the face of it, this seemed like it might be interesting, since all the Buzz! rounds in the main game are already fairly similar - you’re a contestant answering questions using a buzzer. But in practice, nobody ever came up with a new combination of the existing mechanics which sounded very appealing. The costs of making a flexible system are high, but the benefits for this game project would have been low. It wasn’t worth doing.
In many cases, the most cost-effective thing for the project is to proceed by writing code which only implements the immediate design requirements, with just a bit of flexibility to anticipate future changes (and knowing how far to take this is an art). Prototyping and requirements changes will then entail programmers writing new code; if scripting is used, this probably also means writing new support functions and script bindings.
Justified Use of Scripting
In this post, I’ve tried to list the real costs of scripting, from my experience of working on game projects that used it. Scripting should be used to express game behaviour when the benefits to the particular game project justify the costs.
I'd love to hear about other techniques or strategies for using scripting, or any comments about any of the issues I've raised in this post.