Hire Us For Your Game!

Auto-Generating Lua Bindings for Unity C# (MoonSharp)

autocomplete

Arbor Interactive projects have an interesting separation of code and data. A given project may be thought of in two “halves”– the “engine” and the “cloud config”. The former is a Unity project with game logic written in C#, and the latter is a google sheet containing the game’s data (characters, statistics, enemies, icons, sound files, stage layouts, etc).

google sheet config

The Benefits of (Cloud-Config) Lua

In addition to storing and processing image files, audio files, and general text data, the Cloud Config may also store Lua code, as a means of treating logic as data too. Unlike the C# and C++ code that makes the “engine” half of the game run, Lua code may be edited in the cloud and re-downloaded by client apps at runtime, making hot-patching fairly easy.

In fact, it’s easy to imagine a host of beneficial properties for a project that is made mostly of “Cloud Config” Lua–

  • The aforementioned hot-patching.
  • Accessibility of gameplay systems : Interns and beginning programmers may learn Lua faster than they learn C#.
  • Decoupling from Unity : Unity is fantastic technology, but it won’t belong to a scrappy startup forever. The company will likely become public within the next half-decade, or be purchased by one. As their competition with Unreal stabilizes, their focus will likely turn towards ROI and a squeeze isn’t out of the question. It would be nice if we were less reliant on Unity’s technology than we are today, and putting more and more of our tech stack into free-and-excellent software like Lua helps with this.

Our Ugly Current Lua Dev Environment

ugly editing

And this already exhibits several big downsides–

  • No syntax highlighting.
  • No “Find All References” or “Peek Definition” features.
  • Tabbing does not work (it moves the cursor to the next cell).
  • Pressing enter, by default, proceeds to the next cell rather than create a newline (control-enter does this).
  • Absolutely no error checking– to the spreadsheet, this just looks like text.

In other words, we can edit code within the sheet itself but we’ll be missing out on proper IDE features that boost productivity. It’s easy to imagine that this lack of features will cost us far more productivity than the switch to Lua is worth in the first place.

The answer? We’re going to integrate a popular IDE (Visual Studio Code) with our “Cloud Config” spreadsheet “databases” via a Plugin– a process to be documented in a series of posts. In this first post, we tackle the issue of Lua -> C# bindings.

The Problem : “What C# Functions can we Call?”

As a developer writing external scripts, at some point we’re going to wonder what functions are available to us– how can we interface with the game engine?

A cursory glance shows that one common approach is to use hand-made wikis, such as what we see on the Binding of Isaac : Afterbirth wiki.

autocomplete hooks

I’m not a huge fan of this approach, as it requires someone to maintain a website, and it requires the scripter to use their browser to verify something as quick and simple as the correct spelling of an callback event.

The same game also appears to provide a convenient Lua enum that gives the scripter all of the possible event callbacks.

code example

It’s not immediately clear that this code enum is populated by hand, or auto-generated by a script the development team has, and we must avoid the former (because we’re going to be creating a lot of Lua-accessible functions if we’re going to build an entire game from script).

A promising solution then, would be to try and autogenerate things like the above enum, rather than update it by hand.

The auto-generated bindings file

When the auto-generation is finished and working, we would like to produce a file like this–

This file gives us code completion :

code completion file

This snippet of Lua code gives us autocompletion if we’re editing a file within the same directory–

lua visual studio code completion

Visual Studio can suddenly find the functions we need, and tells us a bit about the parameters of the function too. Note that the snippet written here is entirely compatible with C# code written in Unity. For someone trying to transition a codebase from Unity C# to Lua, preserving compatibility as much as possible will expedite the process.

Doing the Auto-Generating : The Editor Script

Arbor Interactive uses MoonSharp within its projects to execute Lua code. It’s a fantastic, open-source interpreter written in C# itself, allowing for easy integration into a Unity project.

moonsharp website

Gameobject lua proxy class A GameObject Lua Proxy class that moonsharp uses to mock Unity’s actual GameObject class. Note the “MoonSharpUserData” attribute up top.

Moonsharp strongly suggests using proxy classes / objects to let Lua Code interact with Unity’s functionality, and so we have a GameObjectLuaProxy class in the image above. The important thing to note is the MoonSharpUserData attribute decorating the class.

The MoonSharpUserData attribute is necessary for MoonSharp to serialize the class instances into a Lua-acceptable form, and so it will be present on every object your developers write that can be accessed from Lua script.

We can use reflection (in an editor script) to find all types in our entire codebase with this attribute…

static List<Type> GetMoonSharpTypes(Assembly a)
{
    List<Type> result = new List<Type>();

    foreach(Type t in a.GetTypes())
    {
        if(t.GetCustomAttributes(typeof(MoonSharp.Interpreter.MoonSharpUserDataAttribute), false).Length > 0)
        {
            result.Add(t);
        }
    }

    return result;
}

The above function will, if given an assembly (which you can get from any type by doing typeof().Assembly), give you a list of the types that are directly accessible from Lua script (assuming you map them into your Moonsharp script’s .Globals array)

Using this function, we can iterate through all of the lua-associated types, their functions, and the associated function parameters, to generate our lua bindings script. Here’s what the code for this looks like–

/* Find all types marked with MoonSharpUserData attribute */
List<Type> lua_proxy_types = GetMoonSharpTypes(typeof(ArborLuaManager).Assembly);

/* Compile a lua bindings file */
string final_lua_file_str = "-- WARNING : DO NOT MESS WITH.\n--THIS FILE HAS BEEN AUTOGENERATED BY ArborLuaBindingsGenerator.cs.\n\n";

foreach (Type t in lua_proxy_types)
{
    string shortened_type_name = t.Name;
    if (t.Name.Contains("LuaProxy"))
        shortened_type_name = shortened_type_name.Remove(t.Name.Length - 8, 8);

    /* Global object */
    final_lua_file_str += shortened_type_name + "={};\n";

    /* Functions */
    MethodInfo[] methods = t.GetMethods();
    foreach(MethodInfo m in methods)
    {
        if (!m.IsPublic)
            continue;
        if (m.DeclaringType != t)
            continue;

        final_lua_file_str += shortened_type_name + "." + m.Name + "=function(";

        /* Get parameters */
        int length = m.GetParameters().Length;
        int i = 0;
        foreach(ParameterInfo p in m.GetParameters())
        {
            if (i < length-1)
                final_lua_file_str += p.Name + "__" + p.ParameterType.Name + ",";
            else
                final_lua_file_str += p.Name + "__" + p.ParameterType.Name;
            i++;
        }

        final_lua_file_str += ")end\n";
    }
}

File.WriteAllText(path_to_lua_directory + "/_bindings_autogen.lua", final_lua_file_str);
Debug.Log("Finished generating Lua bindings.");

The code above needs to be put into a function, of course, but when executed it generates a .lua bindings file that looks like this– auto generated bindings

The last step in the puzzle is to “hook up” this binding generator so that we can forget about it. While it might be tempting to just throw a MenuItem attribute on its function and call it a day, this is error-prone. It is easy to imagine a tired developer forgetting to generate new bindings, or a new intern not knowing about this process at all.

Instead, we’ll make bindings generation an automatic process that we can forget about forever, and will work right out of the box if a teammate clones our project from a repository. The key is to use a new attribute– the DidReloadScripts attribute— to call our bindings file generation code after a change has been made to C# files in our codebase.

didreloadscripts attribute

The DidReloadScripts attribute will cause a function to be executed when Unity finishes compiling changes to C# files within the project, which just so happens to be the moment when a new Lua Binding proxy class may have been introduced. Since the Bindings file generation process is pretty quick, you shouldn’t notice the additional time taken at the end of each compile.

Hurray! We now have a Lua -> C# bindings file that gives us nice script-editing features such as autocompletion, and it updates with new bindings automatically as we associate more and more classes with MoonSharp.

The next step for our Lua scripting environment is to write a plugin so that Visual Studio Code can grab Lua scripts from our Cloud Config google spreadsheets, and push local changes back up to the cloud. Stay tuned for more posts as development progresses.