This cookbook will teach you how to setup NLua in Unity and get Fennel evaluating within your game
To use NLua with unity it requires you install dlls for both KeraLua and NLua. Those repositories explain how to build them, but this will show getting them from NuGet one time as an ease of use.
fennel.lua
library, as described in the setup docs: Embedding the Fennel compiler in a Lua application. Save it in your Unity Assets folder with a .txt
extension (more on this later)Download package
link).nupkg
to .zip
Plugins/KeraLua
and Plugins/NLua
netstandard2.0
contents of those zip files into the newly created unity folders. Your tree should look roughly like this (with KeraLua runtimes customized to your build targets)Plugins
├── KeraLua
│ ├── LICENSE
│ ├── LICENSE.meta
│ ├── lib
│ │ ├── netstandard2.0
│ │ │ ├── KeraLua.dll
│ │ │ ├── KeraLua.dll.meta
│ │ │ ├── KeraLua.xml
│ │ │ └── KeraLua.xml.meta
│ │ └── netstandard2.0.meta
│ ├── lib.meta
│ ├── runtimes
│ │ ├── linux-x64
│ │ │ ├── native
│ │ │ │ ├── liblua54.so
│ │ │ │ └── liblua54.so.meta
│ │ │ └── native.meta
│ │ ├── linux-x64.meta
│ │ ├── osx
│ │ │ ├── native
│ │ │ │ ├── liblua54.dylib
│ │ │ │ └── liblua54.dylib.meta
│ │ │ └── native.meta
│ │ ├── osx.meta
│ │ ├── win-x64
│ │ │ ├── native
│ │ │ │ ├── lua54.dll
│ │ │ │ └── lua54.dll.meta
│ │ │ └── native.meta
│ │ ├── win-x64.meta
│ │ ├── win-x86
│ │ │ ├── native
│ │ │ │ ├── lua54.dll
│ │ │ │ └── lua54.dll.meta
│ │ │ └── native.meta
│ │ └── win-x86.meta
│ └── runtimes.meta
├── KeraLua.meta
├── NLua
│ ├── LICENSE
│ ├── LICENSE.meta
│ ├── lib
│ │ ├── netstandard2.0
│ │ │ ├── NLua.dll
│ │ │ └── NLua.dll.meta
│ │ └── netstandard2.0.meta
│ └── lib.meta
└── NLua.meta
Platform settings
for both editor and standalone based on the folder. Don't forget to applyIn this example we create a NLua GameObject in our scene with an accompanying MonoBehaviour. Notably the Awake
function calls a series of functions to setup your lua instance to be able to eval fennel, as well as load fennel files defined in the inspector fennelScripts
field. It requires you to set the fennel.lua.txt library to the fennelLibrary
field in the inspector at a minimum.
For the purposes of this example we have a single scripts.fnl.txt
file set as a TextAsset in the fennelScripts
field
; Assets/fennel/scripts.fnl.txt
(fn _G.print [...]
(each [_ x (ipairs [...])]
(NLua:Log (tostring x))))
(fn print-and-add [a b c]
(print a)
(+ b c))
{: print-and-add}
And an NLua MonoBehaviour, noting that it copies the files out of your unity build to Application.persistentDataPath
which may not be ideal for your game (or it may if you support modding). An alternative approach could be writing a custom lua package searcher/loader and exposing them from Unity like this project does.
using UnityEngine;
using NLua;
using System.IO;
public class NLua : MonoBehaviour
{
Lua lua = new Lua();
public TextAsset fennelLibrary;
public TextAsset[] fennelScripts;
private void Awake()
{
EnsureFilesExistInPath();
ConnectLuaToCLR();
SetupPathsAndScripts();
Example();
}
/// <summary>
/// This is needed to ensure the fennel.lua library is stored in a path unity and lua can access easily.
/// It also serves loading any .fnl scripts in the same directory.
/// There are many ways to do this, this way lets you keep your lua/fnl scripts contained in the unity build,
/// and work on multiple traget platforms with their conventions.
/// Note that this overwrites the scripts in Application.persistentDataPath every time, you may want to version and check that in production
/// </summary>
void EnsureFilesExistInPath()
{
// fennel.lua Library
File.WriteAllText(Application.persistentDataPath + "/" + fennelLibrary.name, fennelLibrary.text);
// .fnl scripts
for (int i = 0; i < fennelScripts.Length; i++)
{
File.WriteAllText(Application.persistentDataPath + "/" + fennelScripts[i].name, fennelScripts[i].text);
}
}
/// <summary>
/// This allows you to access .Net and Unity from within lua.
/// It sets a reference to this MonoBehaviour for easy access in lua/fnl
/// it also overrides import to prevent loading .Net assemblies (optional, remove if needed)
/// </summary>
void ConnectLuaToCLR(bool preventImport = true)
{
lua.LoadCLRPackage();
lua["NLua"] = this;
if (preventImport) lua.DoString("import = function () end");
}
void SetupPathsAndScripts()
{
// Append package.path with where we stored our .lua/.fnl files
lua.DoString("package.path = package.path .. \";" + Application.persistentDataPath + "/?/?.lua;" + Application.persistentDataPath + "/?.lua\"");
// Require fennel, fix path, and allow us to use require on .fnl files
lua.DoString("fennel = require(\"fennel\")");
lua.DoString("table.insert(package.loaders or package.searchers, fennel.searcher)");
lua.DoString("fennel.path = fennel.path .. \";" + Application.persistentDataPath + "/?/?.fnl;" + Application.persistentDataPath + "/?.fnl\"");
// Load fennel scripts through unity placed files
for (int i = 0; i < fennelScripts.Length; i++)
{
string name = fennelScripts[i].name.Replace(".fnl", "");
Log("Requiring fennel package:" + name);
lua.DoString(name + " = require(\"" + name + "\")");
}
}
void Example()
{
// Example fennel evaluation using overridden print to log in Unity
lua.DoString("fennel.eval(\"(print (+ 1 2 3 4))\")");
// Example fennel evaluation with numeric storage, and retrieval in unity
lua.DoString("result = fennel.eval(\"(scripts.print-and-add :wow 9 9)\")");
double? result = (double?)lua["result"];
if (result.HasValue) Debug.Log("result " + result.Value);
// Example fennel evaluation with string storage, and retrieval in unity
lua.DoString("stringResult = fennel.eval(\":my-string\")");
string stringResult = (string)lua["stringResult"];
if (!string.IsNullOrEmpty(stringResult)) Debug.Log("stringResult " + stringResult);
}
public void Log(object log)
{
Debug.Log("Lua: " + log.ToString());
}
}
This is a basic example of getting the fennel Repl working.
; Assets/fennel/scripts.fnl
; ...
(fn start-repl []
(let [coro (coroutine.create fennel.repl)
options {:readChunk coroutine.yield
:onValues (fn [vals] (NLua:OnValues vals))
:onError (fn [errtype err] (print errtype err))}]
(coroutine.resume coro options)
(fn [input] (coroutine.resume coro input))))
{: print-and-add
: start-repl}
public LuaFunction evalInRepl;
public void SetEvalInRepl(LuaFunction luaFunction) { evalInRepl = luaFunction }
public void OnValues(LuaTable vals)
{
foreach (var item in vals.Values)
{
Debug.Log("OnValues " + item.ToString());
}
}
// Start the repl and save the SetInput reference
lua.DoString("fennel.eval(\"(NLua:SetEvalInRepl (scripts.start-repl))\")");
// Use the evalInRepl reference after some input (hardcoded string here)
evalInRepl.Call("(* 1 2 3)");
C# has a feature known as extension methods, we can use this to simplify our usage of fennel, by introducing the DoFnlString
function on the Lua class
using NLua;
public static class LuaExtensions
{
public static void DoFnlString(this Lua lua, string chunk, string saveTo = null)
{
string prefix = string.IsNullOrEmpty(saveTo) ? "" : saveTo + " = ";
chunk = chunk.Replace("\"", "\\\"");
lua.DoString(prefix + "fennel.eval(\"" + chunk + "\")");
}
}
Now you can do this in your NLua Monobehaviour
// equivalent to calling fennel.eval(...) as we did before
lua.DoFnlString("(scripts.print-and-add :wow 9 9)");
// equivalent to calling result = fennel.eval(...) as we did before
lua.DoFnlString("(scripts.print-and-add :wow 9 9)", "result");
You can better integrate .fnl (and .lua) files in Unity by writing a custom ScriptedImporter. This allows us to drop the .txt extension, but still treat them as a TextAsset
in Unity. In this example we assume a Texture2D asset in an Editor/Resources/Icon
folder for a nice icon (it will still work without)
Note: dropping the .txt extension requires changes to the MonoBehaviour so it saves the file extension of the Fennel and Lua files
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.AssetImporters;
[ScriptedImporter(1, "fnl")]
public class FennelScriptedImporter : ScriptedImporter
{
[SerializeField] private TextAsset fennel;
public override void OnImportAsset(AssetImportContext ctx)
{
fennel = new TextAsset(File.ReadAllText(ctx.assetPath));
ctx.AddObjectToAsset("Fennel Script", fennel, Resources.Load<Texture2D>("Icons/Fennel"));
ctx.SetMainObject(fennel);
}
[MenuItem("Assets/Create/Fennel Script", false, 80)]
public static void Create()
{
string filename = "myfnlmodule.fnl";
string content = "(print :hello-world)\n";
ProjectWindowUtil.CreateAssetWithContent(filename, content);
}
}