At the beginning of this project, I was intent on learning the scripting language for idTech4 (which doesn’t have a more official name), and utilizing it in a level where scripted sequences and AI enemies were controlling the pacing. Having used Radiant, the level editor for idTech4 games before, as well as being familiar with other scripting languages, I figured that this would be a low-risk project. On the whole, I think this was true, although I had forgotten that scripting languages have their own peculiarities and limits that needed to be worked around.
In-editor screenshot of a prior level I had tried making for The Dark Mod.
Setting Up Scripts in Levels:
For this project, I decided to explore scripting for The Dark Mod, a game made in idTech4 that I had played around with making levels for. While it was fun to make spaces and get guards to patrol around them, I wanted to figure out how to hook up scripting so that I could create more interesting sequences of events.
Turns out, getting that set up was pretty easy. Like a scripting project for HPL2 I had done a year prior, level-specific scripts just needed to be the same name as a map file, and be in the same folder in order to be run. Specific functions could then be called using in-level entities called “target_callscriptfunction”, which would be given a string matching the name of the desired function to run. There’s another method that allows scripters to call functions in different script files using an entity called “target_callobjectfunction”, but the communal documentation on class declaration in scripts is vague, minimal, and poorly written.
Properties of idTech4 Scripting Language:
One of the biggest advantages of scripting in idTech4 is how similar it is to C++. This may be more of a help to those who are already familiar with the language, but given that the engine itself was written using C++, I think that it made sense for the developers. Scripts for this engine have a lot of features that are in C-languages, including being able to define classes, class inheritance, and certain preprocessor commands (although I don’t know if these would be useful unless a script was trying to do something extremely complex).
Throughout making the level, it was apparent that scripting things had some major advantages to using in-editor logic entities. There are a lot of things that path_nodes and triggers can accomplish, but using only these prevents access to member functions of other classes. For instance, I can’t tell a trigger to lock or unlock a door when activated; I need to use the door.unlock() function.
In addition, having all of these nodes exist spatially in the level can get messy really quickly. If a scripted sequence were handled exclusively with logic entities, it would be difficult to keep track of which entity led to what, let alone find or click on them in an ever-increasing amount of clutter. This is only exacerbated by having certain path_nodes be position specific (i.e. where they’re placed determines where the AI carries out an activity). Scripting things is a great way of condensing most of that into one easy-to-read function.
An in-editor screenshot of some path nodes handling logic for the first scripted sequence. Had a function not been written to handle animations and waiting, each animation for both guards would need a path_anim node added, as well as a path_wait node for the other guard to stand still during playback.
There are some odd properties to scripting, though. The most basic one is that the language has a float data type, but not an int type. It’s a really minor thing, and anything an int can do can also be done by a float, but I can’t think of a reason why it wasn’t implemented (other than reducing the scope of creating the language, perhaps).
Another small thing was that all functions called in the script must be members of some object. Most of the time this isn’t an issue, but using a global function like “waitFrame()” wouldn’t work. Fortunately, the “sys” (as in “system”) object serves as a way to use these functions. So while the line “wait(3)” would lead to a runtime error, “sys.wait(3)” would work as expected.
There are also certain functions that are conspicuously absent. The biggest one of these is some form of “Update” or “Tick”, allowing conditions to be checked every frame. As such, functions can only be called when events are triggered or on a timer. Some event functions also seem to be limited: as far as I can tell, there’s no way to trigger something when the player “frobs” an object (meaning “use”), or when they pick up an item in the level. There may be indirect ways of doing this using logic entities in the level, but scripting seems incapable of handling this.
The idTech4 scripting language is impressively powerful in certain aspects (class inheritance, preprocessor functionality), but seems to overlook some very basic needs of scripters. While I was able to accomplish almost everything I was trying to do, some of that needed to be done using some odd workarounds, and if someone is trying to do some more complex logic through scripting, I can see how they might be limited. I think learning these tools was valuable, but it’s apparent that level designers are going to need to use scripts in conjunction with in-editor entities in order to get the most out of the engine.