ADVENT CALENDAR 2019

初めての組み込みスクリプティング(C# / Lua 編)

By 檸檬茶(Lemon TEA)

前回の記事では,C#で作成したプログラムにC#で記述されたスクリプトを組み込む方法を示しました. 今回はスクリプトとしてLuaを組み込む方法を示していきます. 主にC++のプログラムに組み込んで使うLuaですが,これをC#で作成したプログラムに組み込むことができるライブラリを見つけました. 今回はそのライブラリを弄くり回して行こうと思います.

なお私の実行環境は以下の通りです.

  • MacOS Catalina 10.15.1
  • Visual Studio for Mac 2019 8.3.10
  • mono 6.4.0.208
  • Lua 5.3.5

今回の記事の作成にあたり,以下の文献を参考にしました.

事前準備

その零 : KeraLuaとは

今回使っていくライブラリはKeraLuaという,C#で作成したプログラムにLuaを組み込めるっょっょなライブラリです. MITライセンスで配布されています. 詳しくは,下記のリンク先のリポジトリをご参照ください.

https://github.com/NLua/KeraLua

その壱 : KeraLuaのインストール

例の如く Visual Studio プロジェクトを作成したら,まずはKeraLuaの導入を行います. KeraLuaはNuGetから入手することができます.

ACAC-2019-12-20(1).png

其の弐 : usingディレクティブ

この記事では,C#にLuaを組み込む際のサンプルソースコードを示していきますが,以下の名前空間をusingしていることを前提としています.

using KeraLua;

Roslynより簡単だね!

実際に組み込んでみる

ここでは実際にC#で作成したプログラムにLuaを組み込んでいくわけですが,実際にすべきこととしては6日目に紹介した方法とほとんど変わりません. また,使用するLuaスクリプトですが,6日目の記事で作成したスクリプトを流用していますので,あらかじめご了承ください.

其の壱 : スクリプトを丸々実行する

ではKeraLuaを使って,まずはLuaスクリプトを丸々実行してみましょう.6日目の記事を思い出してください. C++でLuaスクリプトを実行するときにまず必要なのが,Luaステートの生成です. KeraLuaを用いている場合,KeraLua.LuaクラスのインスタンスがLuaステートに相当します. なので,Luaステートを新たに作成するには以下のように記述します.

Lua state = new Lua()

次に忘れてはいけないのは標準ライブラリの読み込みです. C++に組み込む際はluaL_openlibs(state)と記述しますが,C#でKeraLuaを使っている場合,以下のように記述します.

state.OpenLibs();

標準ライブラリの読み込みが終わったらファイルの読み込みを行います. C++に組み込む際はluaL_loadfile(state)と記述しますが,C#でKeraLuaを使っている場合,以下のように記述します.

state.LoadFile("script.lua");

次に読み込んだスクリプトを実行していきます. C++に組み込む際はlua_pcall(0, 0, 0)と記述しますが,C#でKeraLuaを使っている場合,以下のように記述します.

state.PCall(0, 0, 0);

使い終わったLuaステートはCloseしなければなりません. C++に組み込む際はlua_close(state)と記述しますが,C#でKeraLuaを使っている場合,以下のように記述します.

state.Close();

これらをみて分かる通り,Luaステートに変更を加える際,C++ではそれ用の関数の引数にLuaステートを指定する手法をとりますが,C#ではLuaステートに実装されているメソッドを実行する手法をとっています. オブジェクト指向プログラミングに浸っている人からすれば,C#での手法はC++での手法より何倍もわかりやすいと思います.

以上をまとめると,Luaスクリプトを実行するだけのプログラムは以下のようになります.

// 其の壱 : スクリプトを丸々実行する
//==================================================

using (var state1 = new Lua())
{
    Console.WriteLine("[ 其の壱 : スクリプトを丸々実行する ]\n");

    // OpenLibs メソッドの実行を忘れずに
    state1.OpenLibs();
    state1.LoadFile("script1.lua");
    state1.PCall(0, 0, 0);
    state1.Close();

    Console.WriteLine("\n\n");
}

//==================================================

この部分が実行されると,コンソール画面には Hello world が出力されるはずです. 斯くして,C++に組み込む時と同じような方法で,C#にもLuaを組み込むことができます.

Tips : usingステートメントについて

KeraLua.LuaクラスにはSystem.IDisposableインターフェースが実装されています(System.IDisposableについてはggってください).このインターフェースではDisposeメソッドが定義されており,使い終わったリソースを破棄するために使われます.このインターフェースが実装されているクラスのインスタンスが使い終わった場合,リソース開放時にこのDisposeメソッドを実行すれば良いのですが,スコープ上のある限られた範囲でしか使用しないという場合はusingステートメントを使用することをお勧めします.

using(Lua state = new Lua())
{
    ...
}

この例だと,state変数がスコープを抜けた途端,Disposeメソッドが自動的に実行され,リソースをもれなく解放することができます.Visual Studioを使っていると,System.IDisposableを実装したクラスのインスタンス作成時に警告(っぽいもの)が出てくると思うので,その様なクラスをローカルで使用する場合はusingステートメントを使うようにしましょう.

其の弐 : Luaステートのスタックを利用する

実際にすべきことは,やはり6日目に紹介した方法と全く変わりません.


using (var state2 = new Lua())
{
    Console.WriteLine("[ 其の弐 : Luaステートのスタックを利用する ]\n");

    state2.PushNil();
    state2.PushNumber(2.71);
    state2.PushNumber(3.14);
    state2.PushString("Lua");
    state2.PushBoolean(true);
    PrintStack(state2);
    state2.Close();

    Console.WriteLine("\n\n");
}

なお,PrintStack関数は以下のように実装されているものとします. と言っても,実装内容は6日目の記事のものとさほど変わりません.

// 其の弐 : スタックの内容を表示する関数
static void PrintStack(Lua state)
{
    Console.Write("==================================================\n");

    // 最後に格納した値のインデックスを取得・表示
    // このインデックスがスタックに積まれた値の数である.
    int stack_amount = state.GetTop();
    Console.Write("{0} values are in this stack.", stack_amount);

    Console.Write("\n--------------------------------------------------\n");

    for (int i = stack_amount; i > 0; --i)
    {
        var type = state.Type(i);
        Console.WriteLine(type switch
        {
            LuaType.Nil =>           "NIL           : ",
            LuaType.Boolean =>       "BOOLEAN       : " + state.ToBoolean(i),
            LuaType.LightUserData => "LIGHTUSERDATA : ",
            LuaType.Number =>        "NUMBER        : " + state.ToNumber(i),
            LuaType.String =>        "STRING        : " + state.ToString(i),
            LuaType.Table =>         "TABLE         : ",
            LuaType.Function =>      "FUNCTION      : ",
            LuaType.UserData =>      "USERDATA      : ",
            LuaType.Thread =>        "THREAD        : ",
            _ =>                     "* INVALID VALUE *"
        });
    }

    Console.Write("==================================================\n");
}

ちゃっかりswitch式使ってますね(笑)

この部分が実行されると,コンソール画面には以下の内容が出力されるはずです.

[ 其の弐 : Luaステートのスタックを利用する ]

==================================================
5 values are in this stack.
--------------------------------------------------
BOOLEAN       : True
STRING        : Lua
NUMBER        : 3.14
NUMBER        : 2.71
NIL           : 
==================================================

其の参 : 変数の受け渡し

この項は6日目に紹介したものと全く変わらないため,サンプルソースコードと実行結果のみを示します.

Luaで定義された変数をC#で取得する

// Luaで定義された変数をC#で取得する
using (var state3_1 = new Lua())
{
    Console.WriteLine("[ 其の参(壱) : Luaで定義された変数をC#で取得する ]\n");

    state3_1.OpenLibs();
    state3_1.LoadFile("script3-1.lua");
    state3_1.PCall(0, 0, 0);

    state3_1.GetGlobal("string");
    Console.WriteLine("string = " + state3_1.ToString(-1));
    state3_1.GetGlobal("number");
    Console.WriteLine("number = " + state3_1.ToNumber(-1));
    state3_1.GetGlobal("boolean");
    Console.WriteLine("boolean = " + state3_1.ToBoolean(-1));

    state3_1.Close();

    Console.Write("\n\n\n");
}

実行結果は以下の通りです.

[ 其の参(壱) : Luaで定義された変数をC#で取得する ]

string = This is a lua script.
number = 3.1415925
boolean = True

C#で設定された変数をLuaで使用する

// C#で設定された変数をLuaで使用する
using (var state3_2 = new Lua())
{
    Console.WriteLine("[ 其の参(弐) : C#で設定された変数をLuaで使用する ]\n");

    state3_2.OpenLibs();
    state3_2.LoadFile("script3-2.lua");

    state3_2.PushNumber(56);
    state3_2.SetGlobal("x");
    state3_2.PushNumber(7);
    state3_2.SetGlobal("y");

    state3_2.PCall(0, 0, 0);

    state3_2.Close();

    Console.Write("\n\n\n");
}

実行結果は以下の通りです.

[ 其の参(弐) : C#で設定された変数をLuaで使用する ]

56.0 plus 7.0 is 63.0.
56.0 minus 7.0 is 49.0.
56.0 multiplied by 7.0 is 392.0.
56.0 divided by 7.0 is 8.0.

其の肆 : 関数の受け渡し

Luaで定義された関数をC#で使用する

この項は6日目に紹介したものと全く変わらないため,サンプルソースコードと実行結果のみを示します.

// Luaで定義された関数をC#で使用する
using (var state4_1 = new Lua())
{
    Console.WriteLine("[ 其の肆(壱) : Luaで定義された関数をC#で使用する ]\n");

    state4_1.OpenLibs();
    state4_1.LoadFile("script4-1.lua");
    state4_1.PCall(0, 0, 0);

    state4_1.GetGlobal("arithmetic");
    state4_1.PushNumber(64);
    state4_1.PushNumber(36);

    state4_1.PCall(2, 4, 0);

    Console.WriteLine("sum = " + state4_1.ToNumber(-4));
    Console.WriteLine("dif = " + state4_1.ToNumber(-3));
    Console.WriteLine("mul = " + state4_1.ToNumber(-2));
    Console.WriteLine("div = " + state4_1.ToNumber(-1));

    state4_1.Close();

    Console.Write("\n\n\n");
}

実行結果は以下の通りです.

[ 其の肆(壱) : Luaで定義された関数をC#で使用する ]

sum = 100
dif = 28
mul = 2304
div = 1.77777777777778

C#で定義された関数をLuaで使用する

今回示すサンプルソースコードには注釈があります. まずはそのサンプルをご覧ください.

// C#で定義された関数をLuaで使用する
using (var state4_2 = new Lua())
{
    Console.WriteLine("[ 其の肆(弐) : C#で定義された関数をLuaで使用する ]\n");

    state4_2.OpenLibs();
    state4_2.LoadFile("script4-2.lua");

    state4_2.PushCFunction((IntPtr int_ptr) =>
    {
        Lua state = Lua.FromIntPtr(int_ptr);
        double x = state.ToNumber(-2);
        double y = state.ToNumber(-1);
        state.Pop(-1);
        state.PushNumber(x + y);
        state.PushNumber(x - y);
        state.PushNumber(x * y);
        state.PushNumber(x / y);
        return 4;
    });

    state4_2.SetGlobal("arithmetic");
    state4_2.PCall(0, 0, 0);

    state4_2.Close();

    Console.Write("\n\n\n");
}

11行目に注目してください. PushCFunctionメソッドの引数に指定する関数は,IntPtrクラスのインスタンスを引数にとり,int型の値を戻り値とする関数です. これは要するにグルー関数であり,引数に指定されているのはLuaステートです. 故に,IntPtrクラスのインスタンスをLuaクラスにキャストしなければならないのですが,IntPtrとあってか,GCHandle.FromIntPtrを使いたがる人がいると思いますが,これを使うとプログラムが落ちます. 実はKeraLuaには,グルー関数の引数に指定されたIntPtrクラスのインスタンスをLuaクラスにキャストする専用の関数があります. それがLua.FromIntPtrメソッドです. グルー関数をC#で作成する場合,Luaステートの変更を必要とする場合はLua.FromIntPtrメソッドを使用しましょう.

なお,実行結果は以下の通りです.

[ 其の肆(弐) : C#で定義された関数をLuaで使用する ]

56.0 plus 7.0 is 63.0.
56.0 minus 7.0 is 49.0.
56.0 multiplied by 7.0 is 392.0.
56.0 divided by 7.0 is 8.0.

其の伍 : テーブルの受け渡し

この項は6日目に紹介したものと全く変わらないため,サンプルソースコードと実行結果のみを示します.

Luaで定義されたテーブルをC#で取得する

// Luaで定義されたテーブルをC#で取得する
using (var state5_1 = new Lua())
{
    Console.WriteLine("[ 其の伍(壱) : Luaで定義されたテーブルをC#で取得する ]\n");

    state5_1.OpenLibs();
    state5_1.LoadFile("script5-1.lua");
    state5_1.PCall(0, 0, 0);

    state5_1.GetGlobal("table");
    int table_pos = state5_1.GetTop();
    state5_1.GetField(table_pos, "string");
    Console.WriteLine("string = " + state5_1.ToString(-1));
    state5_1.GetField(table_pos, "number");
    Console.WriteLine("number = " + state5_1.ToNumber(-1));
    state5_1.GetField(table_pos, "boolean");
    Console.WriteLine("boolean = " + state5_1.ToBoolean(-1));

    state5_1.Close();

    Console.Write("\n\n\n");
}

実行結果は以下の通りです.

[ 其の伍(壱) : Luaで定義されたテーブルをC#で取得する ]

string = This is a lua script.
number = 3.1415925
boolean = True

C#で設定されたテーブルをLuaで使用する

// C#で設定されたテーブルをLuaで使用する
using (var state5_2 = new Lua())
{
    Console.WriteLine("[ 其の伍(弐) : C#で設定されたテーブルをLuaで使用する ]\n");

    state5_2.OpenLibs();
    state5_2.LoadFile("script5-2.lua");

    state5_2.NewTable();
    state5_2.PushString("This is a lua script.");
    state5_2.SetField(-2, "string");
    state5_2.PushNumber(3.14159265);
    state5_2.SetField(-2, "number");
    state5_2.PushBoolean(true);
    state5_2.SetField(-2, "boolean");
    state5_2.SetGlobal("table");

    state5_2.PCall(0, 0, 0);

    state5_2.Close();

    Console.Write("\n\n\n");
}

実行結果は以下の通りです.

[ 其の伍(弐) : C#で設定されたテーブルをLuaで使用する ]

string = This is a lua script.
number = 3.14159265
boolean = true

其の陸 : コルーチン

この項は6日目に紹介したものと全く変わらないため,サンプルソースコードと実行結果のみを示します.

using (var state6 = new Lua())
{
    Console.WriteLine("[ 其の陸 : コルーチンを使う ]\n");

    state6.OpenLibs();
    state6.LoadFile("script6.lua");
    state6.PCall(0, 0, 0);

    Lua coroutine = state6.NewThread();
    coroutine.GetGlobal("co");

    while (coroutine.Resume(null, 0) == LuaStatus.Yield)
        Console.WriteLine(coroutine.ToString(-1));

    state6.Close();

    Console.Write("\n\n\n");
}

私の環境でこのコードを実行しましたが,以下のように文字化けしてしまいました.

[ 其の陸 : コルーチンを使う ]

????????????????????????
??????????????????????????????
???????????????????????????????????????????????????

うまいこと文字コードをいじれば以下のように出力されると思います. Luaのコルーチンから返ってきた文字列をちゃんと表示するには何かしら文字コードをいじらなきゃならんのだろうか,それとも英語の文字列を返すしかないのか,うーん…

[ 其の陸 : コルーチンを使う ]

そこは広場だった
小さな滑り台があった
昔ここでよく遊んだことを思い出した

KeraLuaを使うと嬉しいこと

LuaをC++やC#に組み込む強みとしては,ゲームの内部状態をスクリプトから容易に変更可能であることが挙げられます. ただ,9日目の記事で私は「lua_pushcfunction関数ではインスタンスメンバ関数の関数ポインタを指定することができない」と述べました. また13日目の記事では「Roslynを用いたC#スクリプティングでオブジェクトの内部状態を変更するとき,スクリプトに渡すメンバ変数やメンバ関数は全てpublic宣言しなければならないから不便である」と述べました. しかし今回,私がKeraLuaを使って,こんなことを思い立ちました.

LuaオブジェクトのPushCFunctoin関数でC#のインスタンスメンバ関数を指定することができるのでは??

Luaステートのスタックを活用すればprivate宣言されたメンバを含めた内部状態を容易に変化できるのでは??

C++/Lua とか C#/Roslyn とかよりもクソ強なのでは?????

ということで,以下のようなクラスを作成してみました.

using System;
using KeraLua;

namespace KeraLuaSample
{
    // スクリプティング可能なオブジェクトのサンプル
    public class ScriptableObject
    {
        // 内部状態の制御に用いるLuaステート
        private readonly Lua State;
        private readonly Lua Coroutine;

        // 内部情報として平面上の座標をもつ
        private double Position_X;
        private double Position_Y;

        // コンストラクタ
        public ScriptableObject()
        {
            // 変数の初期化
            Position_X = 0;
            Position_Y = 0;

            // スクリプトの読み込み
            State = new Lua();
            State.OpenLibs();
            State.DoFile("scriptA.lua");

            // コルーチンの生成
            Coroutine = State.NewThread();
            Coroutine.GetGlobal("movement");

            // move関数を定義
            Coroutine.PushCFunction(Move);
            Coroutine.SetGlobal("move");
        }

        // オブジェクトが「動く」メソッド
        // Lua側で使用する
        private int Move(IntPtr int_ptr)
        {
            // ステートの取得
            Lua state = Lua.FromIntPtr(int_ptr);

            // 引数の取得
            double Delta_X = state.ToNumber(-2);
            double Delta_Y = state.ToNumber(-1);

            // 内部情報の変更
            Position_X += Delta_X;
            Position_Y += Delta_Y;

            // 一応ステートを空にしておく
            state.Pop(state.GetTop());

            // 戻り値はない
            return 0;
        }

        // オブジェクトを更新する
        public void Update()
        {
            // コルーチンの実行
            Coroutine.Resume(null, 0);
        }

        // 座標をプリントする
        public void PrintCoordinate()
        {
            // プリント
            Console.WriteLine("(" + Position_X + "," + Position_Y + ")");
        }
    }
}

オブジェクトの内部状態の更新はUpdateメソッドで行います. また,内部状態の確認のため,座標をプリントするPrintCoordinateメソッドを定義しています.

なお,scriptA.luaは以下のように記述されているものとします.

function movement()
    while true do
        for i = 1,8 do
            move(3, 0)
            coroutine.yield()
        end
        for i = 1,8 do
            move(0, 3)
            coroutine.yield()
        end
        for i = 1,8 do
            move(-3, 0)
            coroutine.yield()
        end
        for i = 1,8 do
            move(0, -3)
            coroutine.yield()
        end
    end
end

続いて,このクラスを弄るためのコードを記述します. 例えば次のようなものです.

Console.WriteLine("[ Luaを使ってオブジェクトの内部状態を更新する ]\n");

ScriptableObject Object = new ScriptableObject();
for (int i = 0; i < 30; ++i) Object.Update();
Object.PrintCoordinate();
for (int i = 0; i < 20; ++i) Object.Update();
Object.PrintCoordinate();
for (int i = 0; i < 35; ++i) Object.Update();
Object.PrintCoordinate();

Console.Write("\n\n\n");

この場合,1回目のPrintCoordinateメソッドの実行で(0,6),2回目の実行で(18,24),3回目の実行で(9,24)が出力されるという仮説が立ちます. では,この部分を実行したとき,出力結果はどうなるかというと,こうなります.

[ Luaを使ってオブジェクトの内部状態を更新する ]

(0,6)
(18,24)
(9,24)

_人人人人人人人_
> 勝ち申した <
 ̄Y^Y^Y^Y^Y^Y ̄

と,このように仮説通りの結果が出てくれたことで

  • Luaスクリプトに関数を渡すとき,C++に組み込む時と違い,インスタンスメンバ関数を渡すことができる.
  • private宣言されたメンバを含め,内部状態を変更することができる.

と言った考察を立てることができます. やったね!!!

終わりに/次回予告

今回はLuaスクリプトをC#で作成したプログラムに組み込む方法を示してきました. いざKeraLuaをいじってみると,LuaのCAPIを使ったり,C#のRoslynを使った時よりも,想像以上に組み込みの自由度が高いと感じました. C#ということもあり,AltseedとC#で作成したゲームにLuaを組み込むといったことができると思います.

今回作成したサンプルソースコードはgithub情で公開しています. 詳しくはしたのリンク先のリポジトリの「Day20」をご参照ください. https://github.com/GCLemon/ACAC2019

次回の25日目の記事では,C#にIronPythonやIronRubyを組み込んでいく方法を示していきたいと思います. 分量がものすごく大きいですが,もうしばらくお付き合いください……

SHARE THIS POST