【Unity】xLuaの基本的な使い方まとめ

xLuaの基本的な使い方についてまとめました。

Unity2020.2
XLua 2.1.15

xLua?

xLuaはUnityでLuaを取り扱うためのライブラリです。
Tencentが開発しており、MITライセンスで利用することができます。

github.com

インストール

xLuaをインストールするには以下のリリースページからzipをダウンロードします。
展開したフォルダの直下にあるAssetsフォルダ配下のファイルを自身のプロジェクトのAssetsフォルダ配下に移動すればインストール完了です。

github.com

xLuaスクリプトを実行する

それでは早速xLuaのスクリプトを実行してみます。
以下は文字列で定義したスクリプトを実行する例です。

using UnityEngine;
using XLua;

public class Example : MonoBehaviour
{
    private LuaEnv _luaenv;
    
    void Start()
    {
        // LuaEnvのインスタンスを生成
        // これはグローバルなものを一つだけ生成することが推奨
        _luaenv = new LuaEnv();

        // 文字列で定義したLuaスクリプトを実行
        _luaenv.DoString("CS.UnityEngine.Debug.Log('Test')");
    }

    private void Update()
    {
        _luaenv.Tick();
    }

    private void OnDestroy()
    {
        _luaenv.Dispose();
    }
}

xLuaの実行はLuaEnvのインスタンスを通して行います。
これはLua仮想マシンに相当する物であり、パフォーマンスの面から一つだけ生成することが推奨されています。
使い終わったら忘れずにDisposeします。

スクリプトの読み込み方法いろいろ

次にスクリプトの読み込み方法についてまとめます。

文字列を読み込む

まず前節でも行った通り、文字列で指定したスクリプトを実行する方法があります。

_luaenv.DoString("CS.UnityEngine.Debug.Log('Test')");

ただしこの方法は非推奨(後述)であり、基本的には以下のようにファイルを使って読み込みます。

Resourcesフォルダからファイルを読み込む

次にResourcesフォルダに入れたファイルからスクリプトを読み込みます。
まずResourcesフォルダに以下の内容のファイルをtest.lua.txtという名前で保存します。

CS.UnityEngine.Debug.Log("Test")

[ファイル名].lua.txtとする必要がある点に注意してください。
これを以下のようにして読み込みます。

_luaenv.DoString("require 'test'");

再生してTestとログが表示されれば成功です。

ファイルの読み込み方法をカスタムする

ファイルの読み込み方法はカスタムすることが可能です。
Resourcesフォルダ以外から読み込んだり暗号化したりする場合にはこの方法を使います。

まず先ほどのLuaスクリプトファイルをtest.luaという名前にリネームしてAssets直下に配置します。
そして以下のようにカスタムローダを使って読み込みます。

using System.IO;
using UnityEngine;
using XLua;

public class Example : MonoBehaviour
{
    private LuaEnv _luaenv;
    
    void Start()
    {
        _luaenv = new LuaEnv();
        
        // カスタムローダを追加
        _luaenv.AddLoader(CustomLoader);
        
        // Assets/test.luaを読み込む
        _luaenv.DoString("require 'Assets/test.lua'");
    }

    private byte[] CustomLoader(ref string filepath)
    {
        if (File.Exists(filepath))
        {
            return File.ReadAllBytes(filepath);
            
            // 以下でもOK(文字列を取得してUTF8でバイト列に変換)
            //var text = File.ReadAllText(filepath);
            //return Encoding.UTF8.GetBytes(text);
        }
        
        // nullを返すとこのローダでは読み込めなかったとみなされる
        return null;
    }
    
    private void Update()
    {
        _luaenv.Tick();
    }

    private void OnDestroy()
    {
        _luaenv.Dispose();
    }
}

この例ではFileクラスを使ってファイルを読み込んでいますが、どんな読み込み方法でも同じ要領で行えます。
ローダは複数追加することができ、透過的に扱えます。

推奨される実行方法

さてxLuaには公式に推奨されているスクリプトの読み込み方法があります。

まず文字列を直接読み込む方法は非推奨です。基本的にファイルから読み込みます。

またファイルは個別に読み込むのではなく、main.luaのようなエントリポイントになるスクリプトファイルを用意して、
そこからまとめて関連する他のスクリプトファイルを読み込むようにします。

例として、sub.luaをmain.luaから読み込んでみます。
まずsub.lua.txtを用意してResourcesフォルダに入れておきます。

-- sub.lua.txt
CS.UnityEngine.Debug.Log("Sub")

次にmain.lua.txtを用意します。
これにはsubを読み込むスクリプトが描かれています。

-- main.lua.txt
require 'sub'

DoStringメソッドにはこのmainだけを指定します。

_luaenv.DoString("require 'main'");

Subという出力が得られたら成功です。

C#からLuaにアクセスする

グローバルな値を取得する

luaに書かれたグローバルな値はC#から以下のように取得できます。

var exampleVal = _luaenv.Global.Get<int>("exampleVal");
Debug.Log(exampleVal);
グローバルなテーブルを取得する

次にLuaで以下のように定義されたテーブルを取得することを考えます。

exampleTable = {
    exampleVal1 = 123,
    exampleVal2 = "example"
 }

これにはいくつか方法があるのでいかにそれらをまとめます。

クラスや構造体にマッピングする

まずはテーブルに対応する型を持つクラスや構造体にマッピングする方法です。
これを行うには以下のようにします。

// テーブルに対応するクラス(構造体)を定義
public class ExampleTable
{
    // テーブル内の変数と名前と型が一致するpublicなフィールドを定義
    public int exampleVal1;
    public string exampleVal2;
}

// 定義したクラスの型とテーブル名を指定して読み込む
var exampleTable = _luaenv.Global.Get<ExampleTable>("exampleTable");
Debug.Log(exampleTable.exampleVal1); // 123
Debug.Log(exampleTable.exampleVal2); // example

入れ子構造のテーブルにも対応できます。

インターフェースにマッピングする

テーブルをインターフェースにマッピングするにはまずpublicなインターフェースを定義します。
このとき以下のようにCSharpCallLuaアトリビュートを付与しておきます。

[CSharpCallLua]
public interface IExampleTable
{
    int exampleVal1 { get; set; }
    string exampleVal2 { get; set; }
}

次にXLua > Generate Codeを選択してコードを生成します。
今回の例ではIExampleTableを実装したクラスのコードが自動的に生成されます。
作成されたコードはXLua/Genフォルダ配下に配置されます。

後は以下のようにすればテーブルの内容を取得できます。

var exampleTable = _luaenv.Global.Get<IExampleTable>("exampleTable");
Debug.Log(exampleTable.exampleVal1); // 123
Debug.Log(exampleTable.exampleVal2); // example

また、以下のようにテーブル内に関数が定義してあることを考えます。

exampleTable = {
    exampleVal1 = 123,
    exampleVal2 = "example",
    -- 関数
    add = function(self, a, b) 
        return a + b 
    end
}

このような場合には以下のようにインターフェースを定義することでLuaで定義した関数を取得できます。

[CSharpCallLua]
public interface IExampleTable
{
    int exampleVal1 { get; set; }
    string exampleVal2 { get; set; }
    // 関数
    int add(int a, int b);
}
DictionaryやListにマッピングする

テーブルの中から特定の型の変数だけ取り出したいときには以下のようにDictionary型を指定することもできます。

var exampleTable = _luaenv.Global.Get<Dictionary<string, int>>("exampleTable");
foreach (var e in exampleTable)
{
    Debug.Log($"{e.Key}: {e.Value}");
}

また、テーブルを配列のようにして使っている場合を考えます。

exampleTable = {
    1,2,3
 }

このようなケースでは以下のように特定の型だけをリストで取り出すことができます。

// int型の値を全て取得
var exampleTable = _luaenv.Global.Get<List<int>>("exampleTable");
Debug.Log(exampleTable.Count);
foreach (var e in exampleTable)
{
    Debug.Log(e);
}
LuaTableを使う

また以下のようにLuaTable型を使う方法もあります。

var exampleTable = _luaenv.Global.Get<LuaTable>("exampleTable");
Debug.Log(exampleTable.Get<int>("exampleVal1"));
Debug.Log(exampleTable.Get<string>("exampleVal2"));

コード生成なしで柔軟に値を取得できるメリットがありますが、一方で他の方法より処理速度は遅くなります。

グローバルな関数を取得する

次にLuaのグローバルな関数をC#から呼びます。

引数なし戻り値なしであればAction型で取得できる

まず引数も戻り値もないシンプルな関数を考えます。

function exampleMethod()
    print('exampleMethod')
end

このような関数はであれば以下のようにAction型として取得できます。

var exampleMethod = _luaenv.Global.Get<Action>("exampleMethod");
exampleMethod.Invoke();
引数や戻り値がある場合はデリゲートにマッピング

次に以下のように引数や戻り値がある関数について考えます。

function exampleMethod(a)
    print(a)
    return a * 2
end

このような関数は対応するデリゲートをC#で作ってそれにマッピングします。
XLua > Generate Codeを選択してコードを生成する必要があるので注意してください。

// デリゲート
[CSharpCallLua]
public delegate int ExampleDelegate(int a);

// デリゲートに関数をマッピング
var exampleMethod = _luaenv.Global.Get<ExampleDelegate>("exampleMethod");
var result = exampleMethod.Invoke(123);
Debug.Log(result);
関数を返す関数の場合

次に以下のように関数を返す関数をマッピングすることを考えます。

function exampleMethod2()
    print("exampleMethod2")
end

function exampleMethod()
    print("exampleMethod")
    return exampleMethod2
end

このような関数はActionを戻り値とするデリゲートにマッピングできます。

[CSharpCallLua]
public delegate Action ExampleDelegate();

var exampleMethod = _luaenv.Global.Get<ExampleDelegate>("exampleMethod");
var exampleMethod2 = exampleMethod.Invoke();
exampleMethod2.Invoke();
複数の戻り値に対応する

さてLuaの関数は複数の戻り値をとることができます。

function exampleMethod()
    print("exampleMethod")
    return 1, 2
end

これをC#から呼び出すには、以下のようにref引数をとるデリゲートにマッピングします。
一つ目の戻り値はデリゲートの戻り値として、二つ目の戻り値はref引数として受け取ります。

// ref引数をとるデリゲート
[CSharpCallLua]
public delegate int ExampleDelegate(ref int result2);

// マッピング
var exampleMethod = _luaenv.Global.Get<ExampleDelegate>("exampleMethod");
var result2 = -1;
var result1 = exampleMethod.Invoke(ref result2);
Debug.Log(result1);
Debug.Log(result2);
LuaFunctionにマッピングする

関数をマッピングする簡単な方法として、LuaFunctionを使う方法もあります。
この方法はコード生成が不要で手軽ですが、その分パフォーマンスが悪くなります。

var exampleMethod = _luaenv.Global.Get<LuaFunction>("exampleMethod");
exampleMethod.Call();
注意点

C#からLuaを呼びだす際の注意点として以下の2点があります。

パフォーマンス

まずパフォーマンスの観点です。

C#からLuaのデータ、特にテーブルデータや関数にアクセスする際には処理負荷がかかります。
そのためできるだけアクセスの回数を減らすことと、関数はキャッシュしておくことも重要です。

xLuaへの依存

xLuaへの依存を最小限に留めるためにはマッピングにはインターフェースとdelegateを使うべきです。

マッピングをインターフェースとdelegateだけにした上でxLuaの初期化やマッピング周りの処理をまとめておけば、
具象クラスがxLuaに依存することはなくなります。

LuaからC#にアクセスする

次にLuaからC#にアクセスする方法についてまとめます。

C#インスタンスを生成

C#インスタンスは以下のように生成します。
C#側に存在する名前空間やクラス名はそのまま使えますが、名前空間の前にCS.をつける点に注意してください。

-- C#のインスタンスを生成
local gameObj = CS.UnityEngine.GameObject()

-- オーバーロードにも対応
local gameObj2 = CS.UnityEngine.GameObject("example")
型を変数に入れておく

毎回名前空間付きの型を指定するのは面倒なので、以下のように型を変数に入れて使うことができます。

-- 型を変数に入れておく
local GameObject = CS.UnityEngine.GameObject

local gameObj = GameObject()
local gameObj2 = GameObject("example")
static変数、staticメソッド

static変数へのアクセス、staticメソッドの呼び出しは以下のように行います。

local UnityEngine = CS.UnityEngine
local GameObject = UnityEngine.GameObject

-- static変数にアクセス
deltaTime = UnityEngine.Time.deltaTime
print(deltaTime)

-- staticメソッド呼び出し
local camera = GameObject.Find("Main Camera")
print(camera)
メンバにアクセス

メンバへのアクセスは以下のように行います。

local UnityEngine = CS.UnityEngine
local GameObject = UnityEngine.GameObject
local Vector3 = UnityEngine.Vector3

-- メンバ変数
local gameObj = GameObject("Example")
gameObj.transform.position = Vector3.one

-- メソッド
gameObj:SetActive(false)
ref引数の受け渡し

今以下のようにref引数を持つメソッドが定義されているとします。

[LuaCallCSharp] // このアトリビュートについては後述
public class Example
{
    public static int ExampleMethod(int a, ref int b)
    {
        b += a;
        return a + 1;
    }
}

これをLuaから呼び出すにはref引数を渡し、またref引数の結果は二番目の戻り値として受け取ります。

local b = 1000
result, b = CS.Example.ExampleMethod(123, b)
print(result)       -- 124
print(b)            -- 1123
out引数の受け渡し

今以下のようにout引数を持つメソッドが定義されているとします。

[LuaCallCSharp] // このアトリビュートについては後述
public class Example
{
    public static int ExampleMethod(int a, out string b)
    {
        b = "example";
        return a + 1;
    }
}

これを使うには以下のように、out引数を二番目の戻り値として受け取ります。

result, b = CS.Example.ExampleMethod(123)
print(result)       -- 124
print(b)            -- 1123
enum

次にenumの使い方です。C#で以下のようにenumが定義されているとします。

[LuaCallCSharp]
public enum ExampleEnum
{
    One,
    Two
}

これをLuaで使うには以下のようにします。

-- 直接指定
enum1 = CS.ExampleEnum.One
-- 値からキャスト
enum2 = CS.ExampleEnum.__CastFrom(0)
-- 名前からキャスト
enum3 = CS.ExampleEnum.__CastFrom("One")

print(enum1)
print(enum2)
print(enum3)
Delegateを操作する

次に以下のように定義されているDelegateLuaから操作することを考えます。

using System;
using UnityEngine;
using XLua;

[LuaCallCSharp]
public class Example
{
    public static Action ExampleDelegate = () =>
    {
        Debug.Log("Example");
    };
}

Luaからは以下のように扱います。

function example()
    print("Example")
end

-- 登録
--CS.Example.ExampleDelegate = example + CS.Example.ExampleDelegate

-- 実行
--CS.Example.ExampleDelegate()

-- 登録
--CS.Example.ExampleDelegate = CS.Example.ExampleDelegate - example
Eventを操作する

次に以下のように定義されているEventをLuaから操作することを考えます。

using System;
using UnityEngine;
using XLua;

[LuaCallCSharp]
public class Example : MonoBehaviour
{    
    public static event Action ExampleEvent;

    public static void CallEvent()
    {
        ExampleEvent?.Invoke();
    }
}

Luaからは以下のように扱います。

function example()
    print("Example")
end

-- 追加
CS.Example.ExampleEvent("+", example)

CS.Example.CallEvent();

-- 削除
CS.Example.ExampleEvent("-", example)
複雑な型を直接引数に渡す

次に以下のように複雑な型を引数にとるメソッドを考えます。

using UnityEngine;
using XLua;

[LuaCallCSharp]
public class Example
{
    public static void LogVector(Vector3 value)
    {
        Debug.Log(value);
    }
}

これをLuaから呼ぶ際には以下のように引数にテーブルを使うことができます。

CS.Example.LogVector({x = 1, y = 2, z = 3})
型を取得

LuaからC#の型を取得するには以下のようにします。

local type = typeof(CS.UnityEngine.GameObject)
print(type)
その他

最後にその他の細かい注意点や対応している内容についてまとめておきます。

  • LuaC#より型の種類が少ないのでint型とfloat型のオーバーロードなどは区別できない ** どちらか一方だけが呼び出される
  • デフォルト引数にも対応している
  • 可変長引数にも対応している
  • 拡張メソッドにも対応している
  • Genericなメソッドには対応していないので非ジェネリックなメソッドでラップする必要がある

アトリビュートについて

さて上述のソースコードではLuaCallCSharpやCSharpCallLuaといったアトリビュートを使用しました。
この節ではこれらのアトリビュートの使い方について説明します。

CSharpCallLua

このアトリビュートは以下のケースで必要です。

  • Luaの関数をC#のデリゲートにマッピングする際にデリゲートにつける
  • LuaのテーブルをC#のインターフェースにマッピングする際にインターフェースにつける

いずれもこのアトリビュートをつけた上でXLua > Generate Codeを行うことで必要なコード(インターフェースの実装など)が自動生成されます。

LuaCallCSharp

このアトリビュートは、LuaからアクセスされるC#のクラスや構造体につけることで必要なコードが自動生成されます。
付けなくても動きますが、その場合にはリフレクションを使ってアクセスするため遅くなります。

ReflectionUse

上述の通り、LuaCallCSharpをつけない場合にはリフレクションで呼ばれることになります。
ただしリフレクションはIL2PPビルドでコードストリッピングされる可能性があります。

これを防ぐために、コード生成したくないがリフレクションでアクセスしたい場合には、
LuaCallCSharpの代わりにReflectionUseをつけます。
これにより対象の型がlink.xmlに書き出されてストリッピングされなくなります。

LuaからアクセスするC#のコードにはLuaCallCSharpかReflectionUseのどちらからが必ずついているべきです。

staticメソッドで指定することもできる

これらのアトリビュートは上述のように直接付けることもできますが、
以下のようにアトリビュートをつけたい型を返すstaticメソッドを使うこともできます

using System;
using System.Collections.Generic;
using XLua;

public static class ExampleEditor
{
    [LuaCallCSharp]
    public static List<Type> LuaCallCsList = new List<Type>
    {
        // ExampleクラスにLuaCallCSharpアトリビュートを指定したのと同じ効果
        typeof(Example),
    };
}

このstaticメソッドを定義するクラスもstaticにしておく必要がある点に注意してください。
またこのスクリプトはEditorフォルダに配置することが推奨されています。

DoNotGen

LuaCallCSharpアトリビュートをつけているクラスのメンバの一部をコード生成したくない場合にはDoNotGenアトリビュートを使います。
これで指定したメソッド、フィールド、プロパティはコード生成の対象外となり、リフレクションでアクセスされることになります。
指定は以下のように行います。

public static class ExampleEditor
{
    [DoNotGen] 
    public static Dictionary<Type, List<string>> DoNotGenList = new Dictionary<Type, List<string>>()
    {
        // ExampleクラスのtestIntフィールドをコード生成対象外とする
        {typeof(Example), new List<string>() {"testInt"}}
    };
}

このstaticメソッドを定義するクラスもstaticにしておく必要がある点に注意してください。
またこのスクリプトはEditorフォルダに配置することが推奨されています。

BlackList

LuaCallCSharpアトリビュートをつけているクラスのメンバの一部について、リフレクションでもアクセスさせたくないときにはBlackListを使います。
これで指定したメソッド、フィールド、プロパティにはアクセスできなくなります(例えばフィールドだとnullが返ってきます)。
ただしそもそも対象のクラスにLuaCallCSharpアトリビュートがついていない場合には効果がないので注意してください。
指定は以下のように行います。

public static class ExampleEditor
{
    [BlackList]
    public static List<List<string>> BlackList = new List<List<string>> {
        // ExampleクラスのtestIntフィールドにアクセスできなくする
        new List<string>{typeof(Example).FullName, "testInt"}
    };
}

このstaticメソッドを定義するクラスもstaticにしておく必要がある点に注意してください。
またこのスクリプトはEditorフォルダに配置することが推奨されています。

リフレクションによるアクセスについて

さてxLuaではコード生成されていない、かつストリッピングされていない型には、
リフレクション を介してLuaからアクセスできるということになります。

このリフレクションによるアクセスは制限することはできませんが、
NOT_GEN_WARNINGマクロを有効にしておくことでリフレクションによるアクセスが行われたときに警告を出すことができます。

github.com

参考

github.com