Menus in Lua

As a casual game, dialog will be a big part of the user experience. Unfortunately (perhaps, fortunately?) Quake 2 doesn't offer really anything at all in terms of menu or dialog systems. John Carmack famously said that plot in a video game is as important as plot in a porn. I don't know if he still holds this belief, but the design of Quake 2 reflects this: everything is intended to be generic and nothing has character-specific design.

Currently, the only menus to speak of in Quake 2 are the keyboard-only main menu and the console. Both of these are done in very rudimentary systems where the menu items are drawn imperatively in C rather than using a menu system that allows some kind of markdown or XML.

Thankfully, my background is mostly in web development so I spend ALL DAY looking at UI and thinking about the most convenient way for developers to write menus and create callback systems.

The first thing I needed was Lua. I looked around at various options for scripting languages and Lua is simply the easiest to add into a project. The entire C/C++ API is available online and the stack operations make it really simple to use.

The second thing I needed was XML. I found a copy of libXML1 and used that. I chose 1 over 2 because at the time I was still using VC++ 6.0 and libXML2's headers weren't ANSI C (if I remember correctly). libXML1 does everything I need it to and I found a copy of the binaries in some GNU repository with associated documentation as well.

XML

First, I needed to figure out what I wanted the XML to look like. I didn't want to add the complexity of stylesheets quite yet, so all configuration of menus is done in the attribute. The tag names of elements don't do anything yet, so they're mostly there for the developer's convenience.

If you wanted to make a menu that's 800x600, at offset X: 32, Y: 64, with the background image pics/menu.tga then you would write:

<menu
    X=32
    Y=32
    W=800
    H=600
    I="Menu"
    Image="pics/menu.tga"
>
hello world!
</menu>

Then load this menu in using a handful of ways. My preference is to use Lua

Lua

When the game reads a menu's XML, the first thing it does is look for an ID field (in the above example, the ID is “Menu”), and then creates a table within the global Document object at Document.{Id}, so in this case, Document.Menu. In the future I'd like to replace this with a table in either C or Lua that doesn't have global scope but allows users to query for elements in a manner similar to Javascript's DOM.

This new table allows users to create callbacks for their UI. If your menu's ID is “Menu”, then you can write an initialization callback like:

Document.Menu.OnLoad = function()
    print("Hello world!")
end

The OnLoad callback is called after every element in the Menu has been loaded into the Document table. This way, top XML elements will be able to refer to bottom level XML elements, though the bottom level elements will have their OnLoad called after.

In order to associate a Lua script with the XML, the user must add a script tag to the XML. My preference is at the bottom of the XML document:

<menu
    X=32
    Y=32
    W=800
    H=600
    I="Menu"
    Image="pics/menu.tga"
>
hello world!
</menu>
<script Src="scripts/menu.lua" />

As soon as the menu parser hits the script tag, it runs whatever is in that lua file. If your lua file simply contains callbacks, then it will register them, but will not call them until their appropriate time.

Another useful callback that hasn't existed before in Quake 2 is OnClick. When the user clicks on the screen, the engine basically does a recursive AABB check on every element in the menu and returns the ID of the lowest element in the tree to contain the point the user clicked on. It then runs the OnClick callback of the document, which is defined the same way as the OnLoad:

Document.Menu.OnClick = function()
    print("hello world!")
end

In the future I'd like to add an event structure to pass into that callback

Interpolation

So that the template of a menu doesn't have to be tied to its content, I've also added simple, flat, static interpolation. In Lua, anything can open a menu simply by calling OpenMenu(MenuText, Model). The menu text can either be the XML of the menu, or it can be of the format File:path/to/file.xml and this will tell the game to load the XML from the provided filepath.

The Model provides a flat table to interpolate. For example, if your XML reads:

<Menu>
    {{Body}}
</Menu>

Then you can pass the model

{
    Body = "hello world!"
}

And the resultant XML to be parsed will be

<Menu>
    hello world!
</Menu>

In Lua, this would look like

OpenMenu([[
    <Menu>
        {{Body}}
    </Menu>
]], {
    Body = "hello world!"
})

Or something similar

In the future I'd like to un-flatten that structure and add callbacks to the model that don't get interpolated, like OnClick or OnLoad.