Semantics and Syntax of Scripting

The core game mechanics/scripting architecture used in NEON STRUCT is event-based. When an interesting event occurs, an event message is sent to relevant observers to be handled as needed. For example, when the player interacts with an object, that object receives an OnFrobbed event message and may implement a reaction to it. The desk lamp in the screenshot has an OnFrobbed reaction that plays a switching sound effect and toggles its light component on or off.

This event/response design is not unique or original to NEON STRUCT. I first developed it for Eldritch, but event-based systems have been used in games for many years, and this particular implementation was largely inspired by Elan Ruskin’s GDC 2012 presentation, “Rule Databases for Contextual Dialog and Game Logic.” (PDF, 18MB)

Although this event system is expressive and has been useful for me to author the game logic and scripting in two games so far, I recently found myself struggling with the particular syntax I used to described event rules and reactions.

Under the hood (in the C++ library that implements this system), each rule and each action is an object whose members are populated at runtime, like all data in my engine, from a configuration file. (These configuration files are written in a modified INI format and parsed and transformed into a binary format as a content bake step.)

As such, my default way to set up rules and reactions was simply to write an INI block for each object. Here is an actual example, of the reaction which generates an AI noise (i.e., a virtual sound which AIs can “hear” and react to) when an unconscious body lands on the ground after being dropped by the player.

[BodyLandedReaction]
Rule       = "BodyLandedRule"
NumActions = &
@ Action
@@&        = "BodyLandedPlayAINoise"

    [BodyLandedRule]
    Event         = "OnLanded"
    NumConditions = &
    @ Condition
    @@&           = "IsDeadPE"

        [IsDeadPE]
        PEType = "QueryActionStack"
        Key    = "IsDead"

    [BodyLandedPlayAINoise]
    ActionType    = "SendEvent"
    EventName     = "OnAINoise"
    NumParameters = &
    @ Parameter
    @@&Name       = "NoiseRadius"
    @@^Value      = "BodyLandedNoiseRadiusPE"

        [BodyLandedNoiseRadiusPE]
        PEType = "ConstantFloat"
        Value  = 2.5

Despite appearances, this is a fairly simple script which can be stated in one sentence: when the body receives an OnLanded event with the context “IsDead”, it generates an AI noise with a radius of 2.5 meters.

The syntax of the format isn’t especially relevant here. What is important to notice is just how much overhead a simple definition like this one required. In particular, the parts highlighted in red are semantic cruft used to reference each object, and the parts highlighted in blue are syntactic cruft used to build arrays of items, despite each array having only one element in this example.

After a few months of writing increasingly complicated scripts and suffering with the burden of this syntax, I began to consider alternatives. At first, I thought of replacing this system wholesale with a well-tested language like Lua, but that was too risky to do in the middle of a project. I wanted a solution that would not destabilize NEON STRUCT, would continue to support all the existing content, and would expedite future development.

Given those goals, my ideal solution was to design a higher level language which would compile down to the INI file format I was previously writing by hand. Existing scripts would still work. I wouldn’t have to change a single line of C++ code; all the game and engine and tools code would continue to work just as they always had.

As I’m the only intended user of the language, the design phase was extremely simple. Starting with some example scripts similar to the one above, I mocked up new versions in a streamlined syntax. With about half a dozen test scripts, I covered the vast majority of my real world use cases.

Over the past weekend, I wrote a simple compiler in Python. I somewhat underestimated the challenge of that part, thinking that transforming from one textual representation of the data to another would be fairly straightforward. But in order to support the syntax I wanted in my new language, it became necessary to implement a complete compiler with lexing, parsing, and semantic analysis.

And now for the big reveal. Remember that block of crufty INI file definitions I pasted up above? With the new language, that same event reaction can be expressed like so:

Reaction( Rule( "OnLanded", QueryActionStack( "IsDead" ) ) )
{
    SendEvent( "OnAINoise", NoiseRadius = ConstantFloat( 2.5 ) )
}

All the important data is still expressed here—the “OnLanded” event, the “IsDead” context lookup, and the AI noise with the 2.5m radius—and the extra garbage that the game needs to load that data gets automatically generated as a content bake step.

I’ve been using this new language to implement NEON STRUCT‘s introductory level this week, and it has erased virtually all the cognitive overhead of scripting. I even knocked out a few extra scripting tasks I had been putting off for months, just because it was now so easy and pleasant to do.