Writing a Lua DSL For NVLs: Part Three

It seems like forever since revisiting this series. At the end of part two I presented an example script, saying that in the next article (i.e. this one) I would explain its implementation. And so I will!

Before going on I will assume you have read my article on sandboxing Lua scripts.

Our Goal

We want to allow LNVL users to write dialog scripts such as this:

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!",
}

We’ll look at two techniques to help us reach this goal.

Sugar for Method Calls

When we write Lobby "So duck, how goes it?" in the script above we are actually calling a method of the Lobby object. That line becomes this:

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

We use metatables to create this syntax sugar. A metatable in Lua allows us to define custom behavior for our objects, which are all themselves tables. This is Lua’s mechanism for operator overloading. For example, if we attempt to add two tables—let’s say they’re objects of a Vector class—then Lua will check the metatables of each for an __add() function provide meaningful behavior for the operation.

If the example above we use the __call() function of a metatable. This is what Lua invokes if we try to use a table like a function. Here is the implementation that makes the example possible:

-- If we call a Character object as a function then we treat that as a
-- short-cut for calling the says() method.  This can make dialog
-- scripts more readable.
Character.__call = function (character, ...)
    return character:says(...)
end

We cannot get more straight-forward than that. Since Lua allows us to omit parentheses for function calls with only one argument this lets us write dialog in scripts by placing strings right after character names. The combination helps the scripts read more naturally and easily.

Utility Functions Specific to Scripts

The second technique we use for scripts involves the sandboxing I mentioned earlier. Here is an example from a previous article in this series:

AT_THE_BAR = LNVL.Scene:new {

    -- Content here

}

The difference is that this example constructs a scene by calling the LNVL.Scene:new() method. This approach presents a number of problems.

  1. That name is long for its purpose, keeping in mind the intended audience are non-programmers.

  2. The name presents information which the user should not need to remember, e.g. the LNVL prefix.

  3. There is a chance for syntax errors by confusing the period and colon. The method requires both and in a correct order.

We can, and should ease the mental burden of the user by allowing a more simple function name for creating scenes. Earlier in the article we wrote AT_THE_BAR = Scene { … }. It is a lot easier for users to remember Scene than to use LNVL.Scene:new. So we should use Scene as the constructor.

However, we cannot just go into the code for LNVL and define a global Scene function. Since other games include LNVL what is to say they will not have their own function with the same name? The reason we use prefixes such as LNVL.Scene in the first place is to avoid those name collisions. So what we want is a way to make the short function name available in dialog scripts but not have it conflict with anything outside of those bounds. We can do this by modifying the environment in which we execute those dialog scripts.

The code LNVL uses to load dialog scripts looks like this (with some code and comments removed):

-- We sandbox all dialog scripts we load via LNVL.LoadScript() in
-- their own environment so that global variables in those scripts
-- cannot clobber existing global variables in any game using LNVL or
-- in LNVL itself.  This table represents that environment.
--
-- We explicitly define the 'LNVL' key so that scripts can access the
-- LNVL table.  Without that key the scripts could not call any LNVL
-- functions and that would make it impossible to define scripts,
-- characters, or do anything meaningful.

LNVL.ScriptEnvironment = { ["LNVL"] = LNVL }

-- This function loads an external LNVL script, i.e. one defining
-- scenes and story content.  The argument is the path to the file;
-- the function assumes the caller has already ensured the file exists
-- and will crash with an error if the file is not found.  The
-- function returns no value.

function LNVL.LoadScript(filename)
    local script = love.filesystem.load(filename)
    assert(script, "Could not load script " .. filename)
    setfenv(script, LNVL.ScriptEnvironment)
    pcall(script)
end

What is important to note here is how and why we provide the LNVL key for the LNVL.ScriptEnvironment table. Environments in Lua allow us to control what functions a script can use since the script only has access to the keys in the environment table. This offers a path to our goal of providing shorter function names for scripts: we can put functions in the environment table that dialog scripts can use as shortcuts. For example, we can define that shorter Scene function like this:

LNVL.ScriptEnvironment["Scene"] = function (...)
    return LNVL.Scene:new(...)
end

Now dialog scripts can use the shorter name, and because we define this function only for the environment we do not risk any name collisions with the rest of the LNVL engine or any game using it. LNVL defines utilities around this technique, as it simplifies the language for dialog scripts nicely. See the functions CreateConstructorAlias and CreateFunctionAlias for examples of how we use this throughout the engine.

Conclusion?

In this article we have looked at two ways to help simplify our domain-specific language. But are we finished? Certainly not! There are always more improvements to implement, which means this series of articles will see more installments as development on the project continues.

Advertisements

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