Writing a Lua DSL For NVLs: Part Two

Last week I talked about a domain-specific language (DSL) I am creating for the computer game I’m working on. In the first part I briefly discussed some of the high-level questions I asked myself when first approaching the task. But I did not get into any actual examples or code.

In the second part of this series I am going to talk about a feature of the Lua programming language that make it useful for implementing a DSL, using our visual novel engine as an example.

Lua Syntax Shortcuts

LNVL, the visual novel component of our game, represents the narrative scripts using Lua. When we write dialog between characters we are actually writing Lua code. But LNVL provides constructs and shortcuts to create a DSL to make those scripts appear less like pure code.

The DSL relies on a couple of syntactical shortcuts provided by Lua itself. The one we take advantage of the most is the ability to omit the parentheses for function calls in certain situations. Lua lets you drop the parentheses if a function call meets these two criteria:

  1. The function gets only one argument, no more and no less.

  2. That one argument is a literal string or table.

For example:

-- Consider these two function calls.

say("Hello world!")
useFont({path="./path/to/font.tff", size=16})

-- They meet the two criteria described above, so we can drop the
-- parentheses and write them like this.

say "Hello world!"
useFont { path="/path/to/font.tff", size=16 }

Leaving out a pair of parentheses may not seem that important. But if we design our public API—e.g. the functions we use to create characters—with this shortcut in mind then we can create a DSL that eliminates the need for parentheses in almost all situations. Removing those parentheses helps our DSL look more like a data-definition language as opposed to a Lua program where we are calling lots of functions. That is nice because when we are writing dialog we focus on the data we’re building: the scenes, the characters, their chat and monologues, menus of choices to present to the player, and so on. We do not need to think about how the engine processes the data or if it is calling functions behind the scenes. All we want to do is declare the data for our story and let the engine do the rest, and omitting parentheses for function calls wherever possible helps visually hide those ‘behind the scenes’ details.

Function Naming

Being able to omit the parentheses from function calls is useful but we can improve our DSL by carefully choosing our function names. For example, let’s say we have two characters in a script, Lobby and TheDuck, and we want to create a scene where they are speaking to each other. In LNVL that looks like this:

AT_THE_BAR = LNVL.Scene:new{
    name = "At the Bar",

    Lobby:says "So duck, how goes it?",
    TheDuck:says "Quack!",
    Lobby:says "Ehh, what?  What is this?",
    TheDuck:says "Quack!",
    Lobby:says "You, you don't do the English very well, do you?",
    TheDuck:says "Quack!  Quack!",
    Lobby:says "Well terrific...  Bar-wench, another Martini and Rossi!",
}

We create our scene and the dialog within it using two functions: LNVL.Scene:new() and says(). The first accepts a table which lets us wrap the scene’s contents in braces without any parentheses. And the second function takes a string of dialog that each character says, so we can omit the parentheses for that too.

Using the colon in Lua is a common shortcut for calling methods on an object. So these two function calls have the exact same effect:

Lobby:says "So duck, how goes it?"

LNVL.Character.says(Lobby, "So duck, how goes it?")

But the first version is more terse and easier to read. Notice that the method name is a verb in the active voice: says. This is a technique we use to make our DSL appear more data-driven and less like code. It allows us to write lines in our scripts in the format:

Subject:Verb Object

This matches the pattern we use in the English language and that helps improve the readability of the DSL (although we make the assumption the users are English speakers). Another example is the function we use for changing a character’s avatar on screen.

Lobby:becomes "images/lobby/baffled.png"

Being able to omit the parentheses helps us remove visual clutter, but the names we give to functions in our DSL are also vital for crafting the feel and readability of our DSL. We could use a more technically accurate function name like changeImageTo, but when we are writing scripts in the DSL we are not thinking about those technical details. Instead we want the focus to be on what actions a character can perform. And when a character ‘becomes’ another emotion we express that by changing his or her image avatar on screen, hence the becomes function name, which accepts the path to an image as a string.

Next Time

In this article we have taken a short look at how we can structure our DSL by taking advantage of some syntax shortcuts in Lua and careful method names, resulting in something that looks less like Lua code and more specific to our task at hand: creating story scripts. But we can go even farther. Consider the longer scene example above. We can rewrite it like so:

AT_THE_BAR = Scene {
    name = "At the Bar",

    Lobby "So duck, how goes it?",
    TheDuck "Quack!",
    Lobby "Ehh, what?  What is this?",
    TheDuck "Quack!",
    Lobby "You, you don't do the English very well, do you?",
    TheDuck "Quack!  Quack!",
    Lobby "Well terrific...  Bar-wench, another Martini and Rossi!",
}

This removes even more traces of the implementation by getting rid of the full name of the scene constructor method and by replacing says with simply the names of the characters. In the next article we will look at the code we need to write in Lua to support this more concise form of our DSL. Until then, feel free to ask any questions in the comments below.

Advertisements

One thought on “Writing a Lua DSL For NVLs: Part Two

Add Your Thoughts

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s