Fennel wiki: Unity

This cookbook will teach you how to setup NLua in Unity and get Fennel evaluating within your game

Table of Contents

Prerequisites & NLua Installation

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.

  1. Download the 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)
  2. Download KeraLua and NLua from NuGet (use the Download package link)
  3. change the file extensions from .nupkg to .zip
  4. create folders in your Unity project's Assets directory Plugins/KeraLua and Plugins/NLua
  5. extract the 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
  1. Go through each KeraLua runtime liblua54 files in the Unity inspector and set the correct Platform settings for both editor and standalone based on the folder. Don't forget to apply

Unity MonoBehaviour Setup

In 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());
    }
}

Repl

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

Better C# ergonomics

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

Better Unity integration (to drop the .txt extension)

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