uLua v3.0.0
A Lua Modding Framework for Unity.
Lua Modding Framework

uLua is a powerful Lua Modding framework for Unity. It introduces a Lua API and enables modding features for your Unity game.

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

uLua includes the following features:

  • Lua script execution from an external directory or the Resources folder.
  • Event and callback systems which allow events to be invoked in C# and event handlers to be implemented in Lua scripts.
  • Scripts organised in packages which can be easily installed and removed.
  • External asset loading, allowing loading of text, audio clips, images, and Unity asset bundles.
  • An expose sequencer script to expose any game object component to Lua.
  • A base class to use for your Lua API objects.

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

What's New in v3.0

A lot! This release includes significant architectural changes and it would be difficult to list all of the changes. Only the most significant changes or additions are listed in this section. The main intent for this release was to make use-cases and options clearer for users, to minimise dependencies on classes such as LuaMonoBehaviour, and to remove any bloat accumulated over-time with previous releases.

In addition, this release introduces a new feature for external scripts and script packages in the form of asset loading. This feature aims to extend modding capabilities past scripting by allowing users to load and use custom assets within your game.

Base class refactor (e.g. LuaMonoBehaviour)

Previous versions of uLua expected users to make extensive use of the abstract classes ExposedMonoBehaviour / LuaMonoBehaviour for Unity MonoBehaviours, and ExposedClass / LuaClass for other data structures. This design added unnecessary complexity and required significant code changes to fully integrate uLua with an existing project.

In v3.0 this system has been simplified. ExposedMonoBehaviour is deprecated, leaving its functionality fully implemented in LuaMonoBehaviour. In addition ExposedClass / LuaClass were removed completely. The design intent moving forward is for LuaMonoBehaviour to be used more sparingly. LuaMonoBehaviour remains the most feature-complete version of a Lua object, as it can be fully indexed to define fields and event handlers in Lua. However, it no longer needs to be used everywhere. Instead, most use-cases can be handled with regular MonoBehaviours, which can be exposed to Lua using a new component: LuaExposeSequencer.

LuaExposeSequencer

uLua.LuaExposeSequencer is a newly introduced component dictates the name and order in MonoBehaviour instances will be exposed to Lua. The expose instructions can be set in the inspector, allowing game components to be dragged from any object in your scene, a name to be selected, and an expose target to be set, e.g. On Awake, On Load, On Start, etc. When a component is exposed, its type will be registered to Lua as a userdata type. The component will be removed after all instructions are processed.

External Asset loading support

Various asset types can now be loaded externally and accessed from Lua. This new release includes an asset loading cache, a public Lua API for loading, listing, and getting assets, as well as support for external assets to be loaded from scripts or script packages. Assets are loaded asynchronously when pre-loaded through script packages (as specified in the relevant json file), or synchronously as requested at runtime. This is a feature I want to keep evolving, and I hope this first iteration will prove useful enough to highlight new use-cases.

Specifically, the following asset types are supported:

  • images as 2D textures / sprites (png, jpg, jpeg)
  • audio clips (wav, ogg)
  • text assets (txt, csv, xml, bytes)
  • Unity asset bundles (bundle, ab, unity3d)

Code Review

A large part of the codebase has been reviewed for this release. 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 you should expect to run into issues if you are upgrading from a previous version.

Some significant changes are listed below:

  • ExposedMonoBehaviour is now deprecated. All of its functionality is included in uLua.LuaMonoBehaviour.
  • LuaClass and ExposedClass were removed.
  • Some parts of the codebase were re-structured under the uLua.Core and uLua.Packages namespaces.
  • The Lua class is no longer available. Its public functionality has been implemented in the uLua.API class.

Deprecated Code

All previously deprecated and obsolete code has been removed with this update. If you are upgrading from an older version, you will 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

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.API: Class that implements an event handling system and a script execution framework.
  • uLua.LuaExposeSequencer: MonoBehaviour script which exposes other MonoBehaviours to Lua as instructed in the inspector.
  • uLua.LuaMonoBehaviour: 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.

The API class

The uLua.API class sets up various aspects of the uLua framework such as script execution directories and asset loading directories. 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.

Lua scripts and assets

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.

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 "LuaScripts" by default, which means the scene script would have to be placed in the following path:

Resources/LuaScripts/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!

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 "LuaScripts" 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.

External Scripts

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 paths 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 an external script.

External Assets

The newest added feature is the ability to load external assets which can be accessed from Lua scripts.

The following asset types are supported:

  • images as 2D textures / sprites (png, jpg, jpeg)
  • audio clips (wav, ogg)
  • text assets (txt, csv, xml, bytes)
  • Unity asset bundles (bundle, ab, unity3d)

These asset types may be loaded into Lua by placing them into the designated asset folder which is indexed on scene startup:

{ExternalDirectory}/{AssetPath}/

Note: Audio clips cannot be loaded on demand. Instead they can only be pre-loaded asynchronously from within script packages. This is detailed at a later section.

Note: The AssetPath variable is set to "Assets" by default.

Available assets can be queried at runtime using the following method in a Lua script:

GetAvailableAssets()

Similarly, assets from within this folder can be loaded on demand using:

LoadAssetBundle()
LoadSprite()
LoadTextAsset()
LoadTexture2D()

Assets which are loaded are cached, and can be accessed using:

GetAssetBundle()
GetAudioClip()
GetSprite()
GetTextAsset()
GetTexture2D()

The methods listed above return the asset as userdata in Lua. Audio clips, sprites, text assets, and texture 2D assets use Unity Engine's base types in Lua. In contrast asset bundles are exposed to Lua using a Lua-friendly wrapper LuaAssetBundle. See the full documentation for a list of available methods in uLua.Packages.LuaAssetBundle.

Finally C# equivalent methods are also available:

using uLua;
This MonoBehaviour sets up the API framework to your Unity scene. Add to an object in your scene and ...
Definition: API.cs:34
LuaAssetBundle LoadAssetBundle(string filePath, string packageName="")
Loads an asset bundle from the specified external file path.
Definition: API.cs:625
Texture2D LoadTexture2D(string filePath, string packageName="")
Loads a texture2D from the specified external file path.
Definition: API.cs:671
Sprite GetSprite(string filePath, string packageName="")
Get a pre-loaded sprite from external assets.
Definition: API.cs:574
string[] GetAvailableAssets(string extension="", string packageName="")
Get a list of available assets in the specified package.
Definition: API.cs:564
LuaAssetBundle GetAssetBundle(string filePath, string packageName="")
Get a pre-loaded asset bundle from external assets.
Definition: API.cs:529
Sprite LoadSprite(string filePath, string packageName="")
Loads a sprite from the specified external file path.
Definition: API.cs:640
TextAsset GetTextAsset(string filePath, string packageName="")
Get a pre-loaded text asset from external assets.
Definition: API.cs:591
AudioClip GetAudioClip(string filePath, string packageName="")
Get a pre-loaded audio clip from external assets.
Definition: API.cs:546
Texture2D GetTexture2D(string filePath, string packageName="")
Get a pre-loaded texture2D from external assets.
Definition: API.cs:608
TextAsset LoadTextAsset(string filePath, string packageName="")
Loads a text asset from the specified external file path.
Definition: API.cs:656
Namespace containing the uLua project.
Definition: API.cs:16

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/LuaScripts/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 methods are available to use in your code:

using uLua;
static bool ExecuteExternalFile(string file, bool overwriteExisting=false)
Executes a script from the external directory.
Definition: API.cs:692
static bool ExecuteFile(string file, bool overwriteExisting=false)
Executes a script from the resource directory.
Definition: API.cs:732

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.

Finally, to execute code as string input, not from a file, you can use:

using uLua;
static DynValue ExecuteScript(string code, string codeFriendlyName="", bool overwriteExisting=false)
Executes a Lua script.

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

Script Packages

Script packages allow users to organise scripts and assets 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.

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 "LuaScripts/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 are ignored 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

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.

Package Assets

Assets can also be specified in the script package json file, similar to how script contents can be listed. 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",
"LoadAllFiles": true,
"LoadOnDemand": false,
"Assets": [
"Assets/myTextAsset.txt",
"Assets/myTexture2D.png"
]
}

The asset path specified in the "Assets" list is relative to the script package directory, NOT the general asset folder. In this example, the two assets listed in the json file above should be placed under:

Resources/{ScriptPackagesPath}/MyPackage/Assets/

Dependencies

When a script package depends on another package to function correctly, 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.

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:

using uLua;
ScriptPackage GetPackage(string packageName)
Returns the ScriptPackage object for the specified package name. If the package is not found,...
bool LoadPackage(string packageName)
Public function used to load a ScriptPackage on demand.
Definition: API.cs:474
int GetPackageCount()
Returns the number of packages found under the script package path.
string GetPackageName(int i)
Returns the name of the package at the specified index.

and as globals in Lua:

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

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

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.

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:

using uLua;
API.Invoke("PlayerHealthChanged");
static void Invoke(string luaEventName, params object[] args)
Invokes a game event.
Definition: API.cs:272

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

using 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.

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#:

using uLua;
API.RegisterEvent("PlayerHealthChanged");
static void RegisterEvent(string luaEventName, string handlerName="")
Registers an event to the API.
Definition: API.cs:358

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.

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

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#:

using uLua;
API.RegisterEventHandler("PlayerHealthChanged", "HandlePlayerHealthChanged");
static bool RegisterEventHandler(string luaEventName, string handlerName, ILuaObject luaParent=null)
Registers an event handler to be called when an event is invoked.
Definition: API.cs:386

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");

Removing Event Handlers

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

using uLua;
static void RemoveEventHandlers(LuaMonoBehaviour luaParent)
Removes all event handlers registered in a specific context.
Definition: API.cs:419

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.

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");

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 options: the uLua.LuaExposeSequencer and uLua.LuaMonoBehaviour classes.

LuaExposeSequencer is a MonoBehaviour script that can be configured in the inspector. It contains a list of Lua expose instructions, which are configured to expose other game components to Lua.

LuaMonoBehaviour is an abstract class which implements a MonoBehaviour script as a Lua object. It has a built-in instruction for exposure to Lua, and it represents a fully indexable Lua object.

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.

The LuaExposeSequencer script

LuaExposeSequencer can be fully configured from the Unity inspector. It contains a set of instructions which expose specified components to Lua. Any component in your scene can be dragged to an entry in LuaExposeSequencer so that it may be exposed to Lua under the specified name. You may also specify an expose target, e.g. on awake, on start, on scene load etc.

When all expose instructions are completed, the LuaExposeSequencer component is automatically destroyed.

The LuaMonoBehaviour script

uLua.LuaMonoBehaviour 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.LuaMonoBehaviour, all its public members are accessible in Lua. This makes uLua.LuaMonoBehaviour 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 LuaMonoBehaviour:

Player.cs

using uLua;
public class Player : LuaMonoBehaviour {
public int Health = 100;
}
MonoBehaviour script exposed to Lua. You may use this class as a base for your API game objects.
Definition: LuaMonoBehaviour.cs:98

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.LuaMonoBehaviour 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 LuaMonoBehaviour 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 LuaMonoBehaviour name. In addition, it allows multiple LuaMonoBehaviour 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 not be exposed to Lua. To expose the player object to Lua, you must set its expose target to one of the options in the inspector, e.g. on awake, on start, etc. If expose target is set to none or manual, you may expose the object manually by using uLua.API.ExposeToLua<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 : LuaMonoBehaviour {
public int Health = 100;
protected override void OnExpose() {
base.OnExpose();
print ("Player object exposed to Lua!");
}
}
virtual void OnExpose()
Scriptable method called when an object is exposed to Lua.
Definition: LuaMonoBehaviour.cs:307

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 : LuaMonoBehaviour {
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 LuaMonoBehaviour and expose them under a different name in Lua.

Using the LuaParent object

In the previous section we went over the basics of using uLua.LuaMonoBehaviour classes to expose objects to your API. The LuaParent object is an important feature of these classes.

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

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

The LuaParent must be of type uLua.LuaMonoBehaviour, which is the base of uLua.LuaMonoBehaviour. As a result, the LuaParent must be a game object. To set the parent of a uLua.LuaMonoBehaviour game object, you can use its public member LuaParent or the inspector UI.

Using Object Callback Functions

In this section we will go over invoking and implementing callback functions for your API objects. This is a feature 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 method uLua.LuaMonoBehaviour.InvokeLua().

All instances of uLua.LuaMonoBehaviour 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 : LuaMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; }
}

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

function HandlePlayerHealthChanged()
print (MyPlayer.Health);
end
function OnSceneLoaded()
print (MyPlayer.Health);
MyPlayer:Damage(5);
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 (LuaParent.Name) or have a reference to the object, e.g. returned from a function.

Using Object Resource Scripts

Another feature of the uLua.LuaMonoBehaviour class 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.LuaMonoBehaviour, you must enable the EnableObjectScript option in the inspector UI of a certain object.

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/LuaScripts/MyPlayer.lua

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

Overriding, and Hiding Objects and Members in Lua

Overriding Methods

Allowing users to override a method in a modding framework enables mods to significantly change object behaviour. By default, methods defined in LuaMonoBehaviour 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;
using uLua.Core;
public class Player : LuaMonoBehaviour {
[AllowLuaOverride]
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; set; }
}
Definition: ILuaObject.cs:3

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

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.

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 : LuaMonoBehaviour {
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.

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:

using uLua;
static void LoadSavedData(LuaMonoBehaviour targetObject, string index="SaveData")
Loads table data saved for the specified object.
Definition: API.cs:185
static void SaveData(LuaMonoBehaviour targetObject, string index="SaveData")
Saves table data from the specified object.
Definition: API.cs:231

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 of these methods.

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 previously.

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 : LuaMonoBehaviour {
public void Damage(int Damage) {
Health -= Damage;
API.Invoke("PlayerHealthChanged");
InvokeLua("OnDamageTaken");
}
public int Health { get; set; }
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()
-- Update save data table
self.SaveData["X"] = 10;
self.SaveData["Y"] = -10;
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.

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!");

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.

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!