uLua v2.2.0
A Lua Modding Framework for Unity.
uLua: Lua Modding Framework

uLua is a powerful Lua Modding framework for Unity. It enables the development of a Lua API which may be used to mod your Unity game.

uLua wraps around MoonSharp and provides an object oriented Lua Modding framework. It works by setting up an application-wide Lua context and exposing game objects to it. Objects exposed to the Lua context can then be accessed in Lua scripts, allowing users to interact with your game at runtime.

uLua includes the following features:

  • Lua script execution from the Resources folder or an external directory.
  • Base classes to expose your game objects and data structures to Lua.
  • Event and callback systems which allow events and functions to be invoked in C# but implemented in Lua.
  • The ability to organise scripts in packages which can be easily installed and removed.

This project is the result of many hours of hard work. Please support me by leaving a review on the asset store! Follow my Twitter!

What's New in v2.0

Script Packages

Script packages were introduced in 1.3.0 as a means for users to organise external scripts and control their order of execution. In this update, the script packages have been reviewed so that they can be defined in Unity's Resource folders inside a project. A package by the same name may be defined as an external script, overriding the internal definition. You may prevent a script package from being overridden using a json parameter called AllowExternalOverride.

These changes give more options for developers who may want to implement a large part of their game logic in Lua.

Script Execution Order

The overall script execution order was reviewed. Lua scripts are now executed in the following order:

  1. Script Packages (from Resources)
  2. Script Packages (from External directory)
  3. Scene Script (from Resources or External directory)
  4. External Scripts (general)
  5. External Scripts (Scene-specific)

The design intent is for script packages to implement the base game logic and required utilities which subsequent Lua scripts may need.

Registering Events and Event Handlers

The process of registering event handlers has been streamlined. Event handlers are now registered implicitly for all objects but must follow a common naming convention. For example, an event named GameLoaded will implicitly call all functions named OnGameLoaded. In order for the implicit handler registration to work, you must register events early in your project's execution (e.g. on an object's Awake), before any of the event handlers are defined. In addition, all event handlers are automatically removed when an object is destroyed.

These changes make calling RegisterEventHandler and RemoveEventHandlers unnecessary. However, you may still use these functions to explicitly register and remove additional event handlers during runtime.

Registering events is achieved by calling the relevant API method in C#:

uLua.API.RegisterEvent("GameLoaded");

or the built-in Lua function in Lua:

RegisterEvent("GameLoaded");

uLua.API.RegisterEvent() takes a second optional parameter which determines the name of the implicit event handler function. If the second parameter is omitted, the event handler name will default to On{EventName} (e.g. OnGameLoaded in this example). An example of customising the event handler name is shown below.

uLua.API.RegisterEvent("GameLoaded", "GameLoadedHandler");

Built-in Lua functions

You may now customise the default Lua API by disabling built-in functions in the API class inspector. This allows you to select which features you want to use in your Lua code by disabling access to specific functions.

Code Review

A large part of the library has been reviewed for this version. As a result, some variable and class names have been changed, and some variables are no longer available. Efforts have been made to maximise backwards compatibility, but please check the full patch notes if you are running into issues with upgrading.

Some significant changes are listed below:

  • ResourcePath and UserScriptsPath were removed from the API class. These were replaced by a single path variable named ScriptsPath.
  • EnableResourceScript was renamed to EnableObjectScript for Lua objects.
  • "User scripts" are now known as "External scripts" throughout the project.
  • The low level interface IHasLuaIndexer has been renamed to ILuaObject.

Deprecated Code

All previously deprecated and obsolete code has been removed with this update. If you are upgrading from a much older version, you may run into compatibility issues with your previous scripts. This documentation provides up to date instructions on how to use uLua, however, feel free to contact me on suppo.nosp@m.rt@a.nosp@m.ntsof.nosp@m.twar.nosp@m.e.co..nosp@m.uk for additional support.

Dependencies

Resources

Below are some examples of how to use uLua in Unity Engine.

  • Demo Game: A demo game which uses uLua, and comes with its own documentation of the API and source code.

Usage Tutorial

Note: You must install the Unity MoonSharp plugin before you can use uLua. Check the Dependencies section above for a link to the MoonSharp plugin.

uLua consists of the following main scripts:

  • uLua.Lua: A wrapper class providing an application-wide Lua context.
  • uLua.API: Class that implements an event handling system and a script execution framework.
  • uLua.ExposedClass: Class which exposes its instances to Lua. To use as a base for data structures which will be accessible in your API.
  • uLua.ExposedMonoBehaviour: MonoBehaviour script which exposes its instances to Lua. To use as a base for game object components which will be accessible in your API.

The following tutorial is a thorough introduction to the different classes and scripts in the uLua toolkit and will walk you through performing basic tasks with uLua.

1. The API class

The uLua.API class sets up various aspects of the uLua framework such as script execution directories and the event handling system. To start using uLua, add the uLua.API script to an object in your Unity scene. For now, you can stick to the default settings and continue to the next section.

The uLua.API class implements various methods which will be useful as you develop your API. These methods are introduced in the sections below.

2. Executing Lua scripts

Lua scripts may be executed from the Resources folder of your project or from an external directory using the uLua.API class. In the following sections we will go through different approaches to executing Lua scripts.

2.1. Executing a Scene Script

A scene script is a Lua script which is executed when the Unity scene is loaded. To enable scene scripts, use the EnableSceneScript option of the uLua.API class on the inspector UI.

You can think of the scene script as the entry point of your game to Lua. As an example consider that your scene is named MyGameScene. You may create the following script to execute a print command.

MyGameScene.lua

print ("Hello World!");

To execute this script, create a file named MyGameScene.lua and place it at the designated ScriptsPath under the Resources folder in your project.

The ScriptsPath is set to the folder "Scripts" by default, which means the scene script would have to be placed in the following path:

Resources/Scripts/MyGameScene.lua

When you run your scene in the editor, you should see the message "Hello World!" output to the console. Congratulations, you have executed your first Lua script!

2.2. Executing a Scene Script Externally

The uLua.API class allows you to load the scene script from an external directory. To do so, you must use the EnableExternalScripts option of the uLua.API class from the inspector UI and move your scene script file to the external directory under the designated ScriptsPath. The scene script must be placed in a folder which is named after the scene. The directory path would be the following in this example:

{ExternalDirectory}/{ScriptsPath}/MyGameScene/MyGameScene.lua

Note: The external directory is set to Unity's Application.persistentDataPath by default. For more information about this directory path, check the relevant Unity documentation.

Note: The value of ScriptsPath is set to "Scripts" by default.

Place your script in the appropriate directory and run the scene in the editor. You should see the message "Hello World!" output to the console again.

With the EnableSceneScript option enabled, the uLua.API class will attempt to load the scene script from the external directory first. If the scene script is not found in the external directory, it will then be loaded from the Resources folder. This allows users to override the default implementation of scene scripts by redefining them at runtime.

2.3. Manual Script Execution

Scripts can be executed manually in Lua using the built-in API functions or in C# using the uLua.API class.

In Lua, you can use the require() and loadfile() functions to execute scripts placed in the designated ScriptsPath under the Resources folder in your project.

For instance, to execute the following script:

Resources/Scripts/MyScript.lua

You can use one of the following commands:

-- require function
require("MyScript");
-- loadfile function
local MyScript = loadfile("MyScript");
-- The contents of MyScript.lua are not executed until the variable is called as a function
MyScript();

For more information on how to use these commands lookup the Lua programming guide.

In C#, the following two static methods are available to use in your code:

uLua.API.ExecuteFile()

uLua.API.ExecuteExternalFile()

These methods make use of the script execution framework defined by uLua.API. This means that you do not need to specify the execution directories when calling these functions. As long as a game object with your uLua.API settings is set up in your scene, you can execute external or internal Lua scripts by the path to their filename.

Check the full documentation for a list of available parameters and more information on how to use these methods.

2.4. External Scripts

In a previous section we explained how users can override a scene script. While that is useful, there are times when simply extending your scene script is all that is needed.

The uLua.API class can load additional external scripts which are executed after the scene script. To enable external scripts, use the EnableExternalScripts option in the inspector UI.

External scripts are indexed and loaded automatically as long as they are placed in an appropriate path under the external directory. The following paths are parsed for external scripts:

{ExternalDirectory}/{ScriptsPath}/
{ExternalDirectory}/{ScriptsPath}/MyGameScene/

Any scripts found in these path will be executed by the uLua.API class after the scene script is executed. Scripts placed under the MyGameScene folder will only be loaded when that scene is active (i.e. a scene named MyGameScene). All scripts placed in the designated ScriptsPath path will be loaded independently of which scene is loaded.

As an example, you may create the following Lua script and place it in one of those two directories:

MyUserScript.lua

print ("This is an external script.");

Run your scene and you should see the following two messages output to the console:

Hello World!
This is a external script.

2.5. Script Packages

Script packages allow users to organise scripts in groups giving better control over the order of execution of different scripts, as well as easy installation and removal of multiple scripts at once. Packages are parsed and loaded before the base external scripts described in the previous section, and before the scene script.

2.5.1. Script package structure and order of execution

Script packages may be loaded from the Resources folder or from the external directory. For packages to be correctly parsed, they must be placed in a folder under the designated ScriptPackagesPath. For instance, a package named MyPackage may be located in the following external directory:

{ExternalDirectory}/{ScriptPackagesPath}/MyPackage/

or in the following directory under the Resources folder:

Resources/{ScriptPackagesPath}/MyPackage/

Note: The value of ScriptPackagesPath is set to "Scripts/Packages" by default.

Each package folder must contain a json file with the same name which acts as the table of contents for the package. The following is an example of the json file for the MyPackage package:

MyPackage.json

{
"Title": "Script Package Title",
"Description": "This is a description of the script package.",
"Version" : "v0.0.1"
}

Order of execution. Script packages are executed in alphabetical order based on the folder name. To organise script packages in levels of execution, you can add a number followed by a dash at the beginning of the folder name. For example, in the folder structure listed below, MyPackage will always be executed before OtherPackage.

{ExternalDirectory}/{ScriptPackagesPath}/0-MyPackage/
{ExternalDirectory}/{ScriptPackagesPath}/1-OtherPackage/

Note: All characters prior to the first dash in the folder name when parsing for the name of the script package.

If packages are loaded from the Resources folder you have to instead use the filename of the json file to achieve the same effect. For instance:

Resources/{ScriptPackagesPath}/MyPackage/0-MyPackage.json
Resources/{ScriptPackagesPath}/OtherPackage/1-OtherPackage.json

2.5.2. Package contents and order of execution

The properties listed in the previous example of a json file are the title, description, and version of the package which can be used for display purposes. Here, we cover how to define the contents and dependencies of a package.

The package json file allows control of the execution order of scripts within packages. As an example, see the contents tag in the file below:

MyPackage.json

{
"Title": "Script Package Title",
"Description": "This is a description of the script package.",
"Version" : "v0.0.1",
"LoadAllFiles": true,
"LoadOnDemand": false,
"Contents": [
"File1",
"File0"
]
}

In this example we introduce three new properties: Contents, LoadAllFiles, and LoadOnDemand.

Contents: The files listed in the contents tag will always be executed in the order they appear in the list. As a result, the contents tag is used to control the script order of execution within the package. Note that the file extension must be omitted from names in the contents list.

LoadAllFiles: The LoadAllFiles property determines whether all scripts found in the package folder will be executed ("LoadAllFiles": true) or only those specified in the contents list ("LoadAllFiles": false). LoadAllFiles defaults to true if omitted.

LoadOnDemand: The LoadOnDemand property determines the execution behaviour of the package. If set to true, the package will not be executed until explicitly requested. Otherwise, it will be loaded when the scene is loaded. LoadOnDemand defaults to false if omitted.

2.5.3. Dependencies

When a script package depends on another package to function correctly, then the order of execution of the two packages becomes very important. You may define a list of package names as dependencies which, if present, will be loaded before the package contents. An example json file is shown below:

MyPackage.json

{
"Title": "Script Package Title",
"Description": "This is a description of the script package.",
"Version" : "v0.0.1",
"Dependencies": [
"OtherPackage"
]
}

In this case, the API will look for and execute the package named OtherPackage when loading MyPackage.

If a dependency is not found, the script package will not be executed.

2.5.4. Package API Functions

The Lua API maintains a list of installed packages which is accessible for reference and for loading packages on demand. This data is accessible through the C# API class:

uLua.API.GetPackage();

uLua.API.GetPackageCount();

uLua.API.GetPackageName();

uLua.API.LoadPackage();

and as globals in the Lua context:

GetPackage();
GetPackageCount();
GetPackageName();
LoadPackage();

Check the full documentation for a list of available parameters and more information on how to use these methods.

3. Using the Event System

In the previous section we went over the Lua script execution framework of uLua.API. Another main feature of uLua.API is its event system. Events are very useful when implementing game behaviour, and uLua allows you to invoke game events and handle them by implementing event handlers in Lua.

3.1. Invoking Events

Events are invoked by the uLua.API class. The related method is static, which means it can be executed from anywhere in your project. For instance, to invoke an event named PlayerHealthChanged you can use the following command anywhere in your scene:

uLua.API.Invoke("PlayerHealthChanged");

When invoking an event, you may pass any arguments as optional parameters following the event name. For instance:

uLua.API.Invoke("MyEventWithParameters", 5, 1, "text");

You may also invoke an event in a Lua script with the same syntax:

Invoke("MyEventWithParameters", 5, 1, "text");

Any arguments passed to the uLua.API.Invoke() method are automatically handled in Lua and can be included in the event handler definition.

For instance:

function MyHandlerWithParameters(arg1, arg2, arg3)
-- Do stuff with arg1, arg2, arg3
end

You can name your game events whatever you like, however, the same event name must be used in the event handler registration.

3.2. Registering Events

All events must be registered before any event handlers are defined, and before events are invoked. You must register events early in your project's execution, but after uLua.API's Awake function is executed.

Registering events is achieved by calling the relevant API method in C#:

uLua.API.RegisterEvent("PlayerHealthChanged");

or the built-in Lua function in Lua:

RegisterEvent("PlayerHealthChanged");

Event handlers will be registered implicitly for all objects but must follow a common naming convention. For example, an event named PlayerHealthChanged will implicitly call all functions named OnPlayerHealthChanged. In addition, all such event handlers are automatically removed when an object is destroyed.

uLua.API.RegisterEvent() takes a second optional parameter which determines the name of the implicit event handler function. If the second parameter is omitted, the event handler name will default to On{EventName} (e.g. OnPlayerHealthChanged in this example). An example of customising the event handler name is shown below.

uLua.API.RegisterEvent("PlayerHealthChanged", "PlayerHealthChangedHandler");

3.3. Registering Event Handlers

Additional custom event handlers can be registered and removed manually using the uLua.API class. The related methods are static, which means they can be executed from anywhere in your project. To register an event handler for the PlayerHealthChanged event you can use the following command in C#:

uLua.API.RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

This command registers the HandlePlayerHealthChanged global Lua function to be called when the PlayerHealthChanged event is invoked. The event handler HandlePlayerHealthChanged must be defined before the uLua.API.RegisterEventHandler() command is called, otherwise invoking the PlayerHealthChanged event will have no effect.

Note: Keep in mind that event handlers can be registered as members of other objects in Lua by using an optional third argument. For the sake of simplicity, we use a global event handler in this step of the tutorial.

You can now use your scene script to implement the HandlePlayerHealthChanged function.

MyGameScene.lua

function HandlePlayerHealthChanged()
-- Do stuff
end

The methods to register and remove event handlers are also available in Lua. As a result the same event handler may instead be registered in your Lua scene script using a similar syntax. Returning to our previous example:

MyGameScene.lua

function HandlePlayerHealthChanged()
-- Do stuff
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

3.4. Removing Event Handlers

Any additional custom event handlers may be removed by calling the following command in C#:

uLua.API.RemoveEventHandlers();

Or the following equivalent command in a Lua script:

RemoveEventHandlers();

These commands remove all global event handlers.

Note: Again, keep in mind that event handlers are not exclusively globals. Event handlers for a specific object are removed by using an optional argument. For the sake of simplicity, we use global event handlers in this step of the tutorial.

3.5. The SceneLoaded / SceneUnloaded Events

The uLua.API class invokes a few events by default, including the SceneLoaded and SceneUnloaded events.

The SceneLoaded event is invoked after the scene script is executed, and before any external scripts are executed. The SceneUnloaded event is invoked when a scene is unloaded.

You may define an event handler for these events as you would for any other event. For instance, you could add the following event handlers to set up your scene:

MyGameScene.lua

function OnSceneLoaded()
-- Do other stuff
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
function HandlePlayerHealthChanged()
-- Do stuff
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

4. Exposing Objects to Lua

In the previous two sections we have shown how scripts can be structured to handle events invoked in your Unity scene. However, the API that we have set up so far has no game objects exposed to it that would allow us to implement game behaviour.

To expose objects to the Lua API, uLua provides two approaches: the uLua.ExposedClass and uLua.ExposedMonoBehaviour classes. Any script that inherits these classes will automatically expose its instances as objects of the API.

When exposing game objects to the API, the uLua framework uses an object-oriented approach. Each game object is exposed to the API with a unique Lua handle. As we will explore in the following sections, this becomes important when developing your API and structuring your API object scripts.

4.1. The ExposedMonoBehaviour script

uLua.ExposedMonoBehaviour is a MonoBehaviour script. It may be used as a component similarly to MonoBehaviour, but has the added functionality of being available in Lua.

When a class inherits uLua.ExposedMonoBehaviour, all its public members are accessible in Lua. This makes uLua.ExposedMonoBehaviour a good base for developing your API objects.

As an example, let's assume you want to expose your Player object to Lua. For simplicity, let's say the Player class has only one member: its health. You may start by defining a Player script which inherits ExposedMonoBehaviour:

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public int Health = 100;
}

You may now attach the Player script to a game object in your scene. As an example let's name that object MyPlayer.

By default, the uLua.ExposedMonoBehaviour class will use the game object's name to generate a handle in Lua. This means that a game object named MyPlayer in your Unity Scene will be accessible by the same name in Lua. You can specify a different name for your ExposedMonoBehaviour objects by using the Name field in the Unity inspector or in a script. This allows you to separate the game object name from the ExposedMonoBehaviour name. In addition, it allows multiple ExposedMonoBehaviour components to be attached to the same game object, while being accessed as separate objects in Lua. For this tutorial, you should leave the Name field blank.

Note: Game objects may not include spaces or special characters in their name if they are to be exposed to Lua. Object names must follow Lua naming conventions for variables.

By default, the game object containing the Player script will be exposed to the API when the object's Start() method is called. This behaviour may be changed by setting the ExposeOn parameter of uLua.ExposedMonoBehaviour to Awake, SceneLoaded, Manual or to None, allowing you to expose objects manually by using uLua.API.Expose<T>(). Finally, you may override the OnExpose() method in C# to run custom code when an object is exposed to Lua. For example:

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public int Health = 100;
protected override void OnExpose() {
print ("Player object exposed to Lua!");
}
}

The Player script we defined above has a public field named Health. Building on the MyGameScene.lua script from the previous section, the following script shows how the object MyPlayer and its member Health are now accessible in Lua:

MyGameScene.lua

function HandlePlayerHealthChanged()
-- Do stuff
end
function OnSceneLoaded()
print (MyPlayer.Health);
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

The above script will output the health of the object MyPlayer to the console when the scene is loaded.

It is important to note that the Lua object MyPlayer is a reference, not a copy of the Unity object. This means that if the Health member is altered in Lua, its value in Unity will also change.

To make use of the PlayerHealthChanged event, we can build on the Player class design. In the modification below, we implement the Health variable as a property with a public get, and add a public method named Damage(). The Damage() method changes the value of _Health and invokes the PlayerHealthChanged event.

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
}
public int Health { get; }
}

You may then use the Damage() method in Lua, and also make use of the invoked event PlayerHealthChanged as shown below:

MyGameScene.lua

function HandlePlayerHealthChanged()
print (MyPlayer.Health);
end
function OnSceneLoaded()
print (MyPlayer.Health);
MyPlayer:Damage(5);
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

The above example covers the most basic usage of this toolkit. It is recommended you experiment until you find a design that works for your project. For instance, if your Player object is comprised of several components, and you need properties of these components to be accessible in Lua, then you may have to write part of your Player script as a wrapper class, implementing getter and setter properties for its components as necessary for your Lua API. Alternatively, you may implement multiple components as an ExposedMonoBehaviour and expose them under a different name in Lua.

4.2. The ExposedClass script

uLua.ExposedClass is intended for data structures which need to be exposed to the Lua API. It is similar to uLua.ExposedMonoBehaviour, however, the key difference is that uLua.ExposedClass is a simple C# class instead of a MonoBehaviour script.

As an example, you may use uLua.ExposedClass to describe a weapon item in your game. A simple class definition is the following:

Weapon.cs

using uLua;
public class Weapon : ExposedClass {
public int Damage = 0;
// Public constructor
public Weapon(string Name, LuaMonoBehaviour Context = null, bool ExposeOnInit = true, bool EnableObjectScript = false): base(Name, Context, ExposeOnInit, EnableObjectScript) {
}
}

Classes which inherit uLua.ExposedClass need to implement the public constructor as shown in the example code above. This is because the base constructor is used to expose the object to the API and to initialise its Name and Context properties.

Note: We have not yet covered what the Context object is, or how to use the EnableObjectScript parameter. These will be explained at a later section.

Therefore, to expose an instance of Weapon to Lua, all you need to do is instantiate it with the new keyword. In the following example, we instantiate a Weapon named Sword in the Start() method of the Player script.

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
}
public int Health { get; }
private void Start() {
Weapon Sword = new Weapon("Sword");
}
}

As long as this object instantiation is made somewhere in your code, you may access the Sword instance of the Weapon class as a global in Lua. Again, building on the MyGameScene.lua script from the previous section:

MyGameScene.lua

Sword.Damage = 5;
function HandlePlayerHealthChanged()
print (MyPlayer.Health);
end
function OnSceneLoaded()
print (MyPlayer.Health);
MyPlayer:Damage(Sword.Damage);
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

In this example we set the value of the Sword.Damage property to 5, and use that as a parameter when calling the method Damage().

Similarly to uLua.ExposedMonoBehaviour, the class definition given above will expose all Weapon objects to Lua when they are instantiated. This behaviour may be disabled by setting the optional ExposeOnInit parameter of the uLua.ExposedClass constructor to false, allowing you to expose objects manually by using uLua.API.Expose<T>(). An example of a modified constructor is shown below.

Weapon.cs

using uLua;
public class Weapon : ExposedClass {
public int Damage = 0;
// Public constructor
public Weapon(string Name, LuaMonoBehaviour Context = null): base(Name, Context, false) {
}
}

4.3. Using the Object Context

In the previous two sections we went over the basics of using uLua.ExposedClass and uLua.ExposedMonoBehaviour classes to expose objects to your API. The Context object is an important feature of these classes.

The Context object allows you to set a hierarchy for objects exposed to your API and implements a parent-child relationship. If a Context object is not specified, a Lua object is defined as a global by default. This is what we have done so far.

If a Context object is specified, a Lua object is defined as a field of the Context object. This feature may be used for organisation purposes as your API grows in scope.

As an example, we return to the Player and Weapon scripts from the previous section:

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
}
public int Health { get; }
private void Start() {
Weapon Sword = new Weapon("Sword", this);
}
}

In this case we have changed the instantiation of Weapon to include a second parameter which assigns the Player object as the context of Sword. This is indicated by the second parameter (keyword this) in the Weapon() constructor.

To access the non-global Sword object in Lua, you may use the syntax MyPlayer.Sword, as shown below:

MyGameScene.lua

MyPlayer.Sword.Damage = 5;
function HandlePlayerHealthChanged()
print (MyPlayer.Health);
end
function OnSceneLoaded()
print (MyPlayer.Health);
MyPlayer:Damage(MyPlayer.Sword.Damage);
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

Here we have added a command to set the Sword.Damage property to 5 for the Sword object which is in the context of our MyPlayer object.

The Context must be of type uLua.LuaMonoBehaviour, which is the base of uLua.ExposedMonoBehaviour. As a result, the Context must be a game object, and cannot be a uLua.ExposedClass object. To set the context of a uLua.ExposedMonoBehaviour game object, you can use its public member Context or the inspector UI.

4.4. Using Object Callback Functions

In this section we will go over invoking and implementing callback functions for your API objects. This is a feature of both uLua.ExposedClass and uLua.LuaMonoBehaviour which allows callback methods to be invoked in Unity and handled in Lua.

The callback system is similar to the event handling system. The key difference is that callbacks are object-specific. In contrast, event handlers may be defined as globals or as members of any object.

Invoking a callback function is as simple as invoking an API event. It is achieved using the methods uLua.ExposedClass.InvokeLua() and uLua.ExposedMonoBehaviour.InvokeLua().

All instances of uLua.ExposedClass and uLua.ExposedMonoBehaviour invoke the "OnLoad" and "OnExit" callbacks by default. "OnLoad" is invoked after the object is first exposed to the API. "OnExit" is invoked when an object is destroyed.

You may invoke these callbacks as needed in your code, and you may invoke custom callbacks as well. In the following piece of code I have invoked the OnDamageTaken callback in the Player script.

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; }
public void Start() {
Weapon Sword = new Weapon("Sword", this);
}
}

You may implement this callback function anywhere in Lua. The callback function must be a member of a Player object, because it is invoked by the Player script. In this case, our Player object is named MyPlayer, so we implement the OnDamageTaken callback function as member of MyPlayer.

MyGameScene.lua

MyPlayer.Sword.Damage = 5;
function HandlePlayerHealthChanged()
print (MyPlayer.Health);
end
function OnSceneLoaded()
print (MyPlayer.Health);
MyPlayer:Damage(MyPlayer.Sword.Damage);
end
function MyPlayer:OnDamageTaken()
print (self.Health);
end
function OnSceneUnloaded()
RemoveEventHandlers();
end
RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");

In Lua, the ":" syntax in MyPlayer:OnDamageTaken() is used as syntactic sugar to mimic object oriented design. Defining the callback function as MyPlayer:OnDamageTaken() allows use of the self keyword to access members of this object.

In our example, the event PlayerHealthChanged and the callback function OnDamageTaken achieve the same effect. There are operational differences between the two so you must choose whether to use a callback function or an event on a case by case basis. The main differences between the two options are the following:

  • Event handlers may be defined as globals or as a member of any object. Callback functions are object-specific.
  • To implement a callback function, you must know the Lua handle of an object (Context.Name) or have a reference to the object, e.g. returned from a function.

4.5. Using Object Resource Scripts

Another feature of the uLua.ExposedClass and uLua.ExposedMonoBehaviour classes is the ability to execute a Lua script for each object that is exposed to the Lua API. You may use this feature to implement base functionality for your game objects which cannot be modified by external scripts.

  • To use this feature for instances of uLua.ExposedMonoBehaviour, you must enable the EnableObjectScript option in the inspector UI of a certain object.
  • To use this feature for instances of uLua.ExposedClass, you must use the EnableObjectScript parameter in the constructor definition of that class. For an example refer to previous implementation of a Weapon class in section 4.2.

When the object resource script is enabled, uLua will execute a Lua script by the name of the object after it is exposed to the API. Object scripts are executed from the ScriptsPath under the resource directory in your project.

For instance, for a Player object named MyPlayer, you may place a resource script in the following path:

Resources/Scripts/MyPlayer.lua

Note: This is assuming that the ScriptsPath is set to its default value of "Scripts".

4.6. Overriding, and Hiding Objects and Members in Lua

4.6.1. Overriding Methods

Allowing users to override a method in a modding framework enables mods to significantly change object behaviour. By default, methods defined in ExposedClass and ExposedMonoBehaviour scripts may not be overridden in Lua.

To allow a method to be overridden in Lua, you may use the the [AllowLuaOverride] attribute. The use of this feature is limited to method calls made in a Lua script. If an overridden method is called in C#, its original implementation will be called instead.

An example is shown below for the Damage() method.

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
[AllowLuaOverride]
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; set; }
public void Start() {
Weapon Sword = new Weapon("Sword", this);
}
}

You may now use a Lua script (e.g. the object resource script) to override the implementation of that method.

MyPlayer.lua

function MyPlayer:Damage(Value)
self.Health = self.Health - Value;
Invoke("PlayerHealthChanged");
end

4.6.2. Hiding Objects from External Scripts

It is often useful to hide certain objects of the API from users. To achieve this, you may use the ExternalBlacklist feature. This is available in the API class through the Unity inspector. Adding the name of a global object to the ExternalBlacklist list will make it unavailable to external scripts at runtime.

This feature may be used to hide objects of the API from users, while keeping them available to use in-engine.

4.6.3. Hiding Members

Sometimes it is unsafe to expose certain class members to your Lua API. To prevent a public member from being exposed to Lua, you may add the relevant MoonSharp attribute to a class:

[MoonSharpHideMember("MemberName")]

For instance, if you wanted to hide the Awake() method of the Player script described above, you may use the following syntax:

Player.cs

using MoonSharp.Interpreter;
using uLua;
[MoonSharpHideMember("Start")]
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; set; }
public void Start() {
Weapon Sword = new Weapon("Sword", this);
}
}

This would prevent the Start() method from being called within the Lua API. This could also be achieved by defining the Awake() method as protected or private. However, that would not be an option in cases where a specific member must be public so that other scripts in your project can have access to it.

5. Saving and Loading Object Data

The uLua.API script allows you to save variables of a Lua API object into a file which can then be loaded again at runtime. This feature may be used to save settings or other information and load it seamlessly back into your API in-between playthroughs.

To save data and load for a Lua object, you can use the following commands anywhere in your scene:

uLua.API.SaveData();
uLua.API.LoadSavedData();

These methods take 2 arguments:

  • LuaMonoBehaviour Object: The object for which data will be saved or loaded.
  • string Index: (Optional) The index of the data to be saved or loaded. Default value is "SaveData".

For more information, check out the full documentation below:

uLua.API.SaveData();

uLua.API.LoadSavedData();

You may save any number of variables, however, the save data must be in the format of a Lua table. The data with the index "SaveData" is saved by default, unless specified otherwise by the second argument of SaveData() and LoadSavedData().

For instance, the following Lua table could be used to store the position of the MyPlayer object we defined in section 4.

MyPlayer.SaveData = {
["X"] = 10,
["Y"] = -10,
}

The members of the table determine what information will be saved. Nested tables are not supported.

The SaveData table may be defined anywhere in Lua:

  • in a scene script,
  • in an object script, or
  • in external scripts.

You can then call the the saving/loading methods as required in your code. For instance, you may use Unity's Start() and OnDestroy() methods. Returning to the previous example of a Player script:

Player.cs

using uLua;
public class Player : ExposedMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; set; }
public void Start() {
Weapon Sword = new Weapon("Sword", this);
}
protected override Start()
base.Start();
API.LoadSavedData(this);
end
protected override void OnDestroy() {
base.OnDestroy();
API.SaveData(this);
}
}

These methods are also available in the Lua API as globals with the same arguments:

SaveData();
LoadSavedData();

and may be implemented similarly in OnLoad and OnExit callbacks. For example:

MyUserScript.lua

function MyPlayer:OnLoad()
LoadSavedData(self);
end
function MyPlayer:OnExit()
SaveData(self);
end

Keep in mind that in both cases the MyPlayer.SaveData table always has to be updated with the current values of X and Y before calling the SaveData() method. The Lua callback OnExit is a suitable place to do this in both cases.

6. Message, warning, and error logging

You may use the following commands for message logging and error reporting in Lua:

print("Hello World!");
LogWarning("Hello World!");
LogError("Hello World!");

These commands are linked to Unity's corresponding Debug.Log(), Debug.LogWarning(), and Debug.LogError(), which means that any messages printed in Lua will also be printed to the console in the Unity editor.

Finally, these commands trigger the following events in Lua:

LuaMessageLogged
LuaWarningLogged
LuaErrorLogged

The first parameter passed in an event handler for these events contains the string that was logged. For instance:

function OnLuaMessageLogged(Text)
-- 'Text' contains the string "Hello World!"
-- Do stuff
end
print("Hello World!");

7. Visual Studio Code Debugger

uLua supports the Visual Studio Code debugger for MoonSharp. To use the debugger you must install Visual Studio Code and download the MoonSharp Debug extension from the Visual Studio marketplace. The MoonSharp Debug extension is not actively being developed, however, it works if set up correctly.

To set up the debugger follow the instructions below:

  • Make sure that your MoonSharp installation includes 'MoonSharp.Debugger.VsCode.dll'. The MoonSharp package on the Unity Asset Store already includes this.
  • Install Visual Studio Code and the MoonSharp Debug extension from the Visual Studio marketplace.
  • Create a workspace in Visual Studio Code with your Lua scripts. Your workspace can include both resource scripts and external scripts from different folders.
  • Create a file called 'launch.json' and place it inside the '.vscode' folder in your workspace directory. Copy the contents listed below to your 'launch.json' file.

**.vscode/launch.json**

{
"version": "0.2.0",
"configurations": [
{
"debugServer" : 41912,
"name": "MoonSharp Attach",
"type": "moonsharp-debug",
"request": "attach"
}
]
}

Visual Studio Code Debug uses the 'launch.json' file to attach the debugger to your Lua scripts, so it is critical that it is set up correctly. It is worth noting that official documentation on the structure of the 'launch.json' file is incorrect, so make sure you use the structure given here.

  • Enable the Visual Studio Code Debugger in your Unity scene by selecting the option 'Enable Debugger' on the API object inspector.
  • Start your scene in the Unity editor or execute your built game.
  • Attach the debugger in Visual Studio Code by selecting 'Run -> Start Debugging' on the Visual Studio Code menu or pressing (F5). If the debugger is attached correctly, you will see Lua scripts listed in the 'Debug Console' window in Visual Studio Code as they are executed.

At this point you will be able to use breakpoints, pause/continue execution, and step through your Lua scripts in Visual Studio Code. The debugger is not perfect, but it works.

The debugger relies on finding the Lua script files in a directory in order to apply breakpoints. If a file is not found during execution (e.g. a resource file in a compiled version may not be present on the hard drive), MoonSharp creates a temporary copy of the script which you can step through. The location of these temporary files is listed in the debug console as they are executed. You may still use breakpoints in the temporary scripts, but you must use the file generated by MoonSharp.

For Lua scripts located in Unity resource folders, it is important that the MainResourcesFolder variable in your API object is set correctly in the inspector. uLua uses this path to locate the resource scripts for debugging, so it is advised that you place all your scripts in the same resources folder. If your Lua scripts are split between multiple resource folders within your project, the debugger will not be able to locate all of them and will create temporary copies of the scripts for debugging.

8. Further Support

This covers a large portion of the features in uLua! I hope that this tutorial together with the documentation are enough to get you started on your API development. However, for any further questions do not hesitate to contact suppo.nosp@m.rt@a.nosp@m.ntsof.nosp@m.twar.nosp@m.e.co..nosp@m.uk.

I have put together a demo paddle game project which utilises uLua, and which comes with its own documentation of the API and source code. You may find it here.

This project is the result of many hours of hard work. Please support me by leaving a review on the asset store! Also, follow my Twitter!