* 目次 [#d070a7cf]
#contents
* Moonsharpとは [#df0cc95f]
一言でいえば、レベルデザイン必要なゲームを開発するなら、入れておきなさいツール。
わかりやすくいうと、これを入れない設計でゲームをつくると、「ちょっ変更するだけなのにビルド時間こんなにかかるの?!」という事態になってしまう。
UnityでLuaを使えるようにするプラグインである。
Asset Storeを開き、
MoonSharp
と検索すると出てきます。
https://assetstore.unity.com/packages/tools/moonsharp-33776?locale=ja-JP
MoonSharpは、.NET、Mono、Xamarin、Unity3Dプラットフォーム向けに完全にC#で書かれた完全なLuaソリューションです。
* 特徴 [#g6a14b51]
- Lua 5.2との99%の互換性(サポートされていない唯一の機能はウィークテーブルのサポート)
- metaluaスタイルの無名関数(ラムダスタイル)のサポート
- 使いやすいAPI
- Visual Studio Codeのデバッガサポート(PCLターゲットはサポートされていません)
- Webブラウザとフラッシュでアクセス可能なリモートデバッガ(PCLターゲットはサポートされていません)
- .NET 3.5、.NET 4.x、.NET Core、Mono、Xamarin、Unity3Dで動作
- iOSのようなAhead-of-timeプラットフォームで動作
- IL2CPPに変換されたコードで動作
- .NET 4.xのポータブルクラスライブラリを必要とするプラットフォーム(Windows Phoneなど)で動作
- 外部の依存関係がなく、可能な限り少ないターゲットで実装
- サポートされている場合、ランタイムコード生成によるCLRオブジェクトとの簡単でパフォーマンスの高いインターオペラビリティ
- メソッド、拡張メソッド、オーバーロード、フィールド、プロパティ、インデクサーとのインターオペラビリティがサポートされています
- 非常に少ない例外(主に「debug」モジュールにある)と少数の拡張(主に文字列ライブラリ内)を除いて、完全なLua標準ライブラリのサポート
- .NET 4.xターゲット用の非同期メソッド
- 難読化とランタイム時のより迅速な解析のためのバイトコードのダンプ/ロードをサポート
- JSONとLuaテーブルの間で変換するための(依存関係のない)組み込みのJSONパーサー
- スクリプトがアクセスできるものをサンドボックス化するための、Lua標準ライブラリモジュールの簡単なオプトアウト
- 使いやすいエラー処理(スクリプトエラーは例外です)
- コルーチンのサポート(C#イテレータとしてのコルーチンの呼び出しを含む)
- REPLインタプリタ、および数行のコードで独自のREPLを簡単に実装するための機能
- http://www.moonsharp.org にある完全なXMLヘルプとチュートリアル
* MoonSharpと標準Luaの違い [#e0052ec6]
http://www.moonsharp.org/moonluadifferences.html
を参照してください。
ダウンロード、情報、チュートリアルなどについては、http://www.moonsharp.org をご覧ください。
*** ライセンス [#p3b97abd]
プログラムとライブラリは、3条項BSDライセンスの下でリリースされています - ライセンスセクションを参照してください。
文字列ライブラリの一部は、KopiLuaプロジェクト(https://github.com/NLua/KopiLua)に基づいています。デバッガーのアイコンは、Eclipseプロジェクト(https://www.eclipse.org/)からのものです。
* 使用方法 [#rc0da492]
ライブラリの使用は次のように簡単です:
double MoonSharpFactorial()
{
string script = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end
return fact(5)";
DynValue res = Script.RunString(script);
return res.Number;
}
より詳細なチュートリアル、サンプルなどについては、http://www.moonsharp.org/getting_started.html を参照してください。
** 公式サイト [#ae77951f]
https://www.moonsharp.org/
*** Github [#h2f29b43]
https://github.com/moonsharp-devs/moonsharp
* チュートリアル [#e115d97d]
本家のチュートリアルを翻訳しました。
** 元記事 [#m4521d40]
https://www.moonsharp.org/getting_started.html
** チュートリアルをはじめるにあたって [#dc75bb7e]
このチュートリアルでは、UnityでMoonSharpの簡単さと強力さを感じていただけるでしょう。MoonSharpを使用するには他にも良い方法がありますが、これが最も簡単な方法です。他のチュートリアルではさらに掘り下げて、UnityでMoonSharpを最大限に活用する方法を学ぶことができます。
とりあえず、始めましょう。
このチュートリアルでは、LuaとC#を知っていることを前提としています。Luaの基礎を知らず、C#について十分な知識がないと、サンプルを理解するのは難しいでしょう。
*** ステップ1:UnityでMoonSharpを入手する [#sfc07f66]
最初のステップは、UnityでMoonSharpを入手することです。
Unity Asset Storeのパッケージは現在承認待ちで、承認され次第このURLから入手できるようになります。承認されたら、これを使用するのがUnityでMoonSharpをインストールするのに推奨される方法になります。
MoonSharp配布物のinterpreter/net35フォルダに含まれているMoonSharp.Interpreter.dllをAssets/Pluginsフォルダに入れます。
Windows StoreアプリやWindows Phoneのサポートが必要な場合は、MoonSharp配布物のinterpreter/portable-net40フォルダにあるMoonSharp.Interpreter.dllをAssets/Plugins/WSAフォルダにコピーします。
次に、このガイドに従ってください。
その後、Unity EditorからMoonSharp.Interpreter.dllを参照として追加します。
注:IL2CPPプロジェクトでMoonSharpを使用する場合は、以下の内容でAssetsディレクトリ内にlink.xmlを作成または編集してください。
<linker>
<assembly fullname="MoonSharp.Interpreter">
<type fullname="MoonSharp.*" preserve="all" />
</assembly>
</linker>
*** ステップ2:名前空間のインポート [#m972361d]
コードの先頭に以下の行を追加して、MoonSharp.Interpreter名前空間をコードにインポートします。
using MoonSharp.Interpreter;
*** ステップ3:スクリプトの呼び出し [#u47bd259]
ここでは、MoonSharpを使用して階乗を計算するMoonSharpFactorial関数を作成します。
double MoonSharpFactorial()
{
string script = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end
Copy code return fact(5)";
DynValue res = Script.RunString(script);
return res.Number;
}
*** ステップ4:完了! [#nf07c737]
もちろん、コードを実行するには、Unity C#スクリプトの他の場所でMoonSharpFactorial関数を呼び出すだけです。
これで、UnityでMoonSharpを使うための準備が整いました。他のチュートリアルに進んで、UnityでMoonSharpを最大限に活用する方法を学びましょう。
** スクリプトを保持する [#re6a0baf]
前回のチュートリアルでは、MoonSharpに初めて触れました。スクリプトを文字列に入れ、コードを実行し、その出力を取得しました。これは時には便利かもしれませんが、ほとんどのユースケースに必要な相互運用性には、CLRコードとMoonSharpの間でもう少し統合が必要です。
*** ステップ1: 前回のチュートリアルで行ったことをすべてやり直す [#w629e4ff]
真面目な話、ここではもう少し高度な概念を学んでいますが、最初の試みで捨てるべきものはほとんどありません。それは素晴らしい出発点です。
*** ステップ2: コードを変更してScriptオブジェクトを作成する [#vf907cb2]
最初の変更点は、静的メソッドを使用する代わりにScriptオブジェクトを作成することです。ロケット科学ではありませんが、次の進化に向けて準備をしています。
double MoonSharpFactorial()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end
return fact(5)";
Script script = new Script();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
*** ステップ2: グローバル環境にアクセスする [#te57efec]
スクリプトオブジェクトを持っているので、関数が動作するグローバル環境を変更できます。
例えば、fact関数への入力を変更して、プログラムがどの数の階乗を計算するかを指定できるようにしたいかもしれません。
double MoonSharpFactorial()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end
return fact(mynumber)";
Script script = new Script();
script.Globals["mynumber"] = 7;
DynValue res = script.DoString(scriptCode);
return res.Number;
}
ご覧の通り、script.Globalsテーブルを単純な構文で参照するだけで、数値をMoonSharpスクリプトに注入できます。実際には数値以上のものを渡すことができますが、今のところは数値、ブール値、文字列に限定しましょう。関数やオブジェクトを渡す方法は後で見ていきます。
*** ステップ2: 関数を直接呼び出す [#ya81cef6]
外部から選択した数値の階乗をMoonSharpで計算する方法を学びました。
でも、そのようにやるのは汚いハックのように見えます(それでも、これからよく使う重要なテクニックです)。
以下は、C#からLua関数を呼び出す方法です。
double MoonSharpFactorial2()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end";
Script script = new Script();
script.DoString(scriptCode);
DynValue res = script.Call(script.Globals["fact"], 4);
return res.Number;
}
何をしたのか見てみましょう。
まず、スクリプトの最後のreturnを削除しました。
残しておくこともできましたが、任意のパラメータでfactを呼び出したいので、無駄になるはずでした。
この時点で、
script.DoString(...)
の呼び出しはファイルを実行し、グローバル環境にfact関数を残します。
そして、この行を導入しました。
DynValue res = script.Call(script.Globals["fact"], 4);
これは、スクリプトのグローバルからfact関数を取得し、4をパラメータとして呼び出します。
もっと良い(つまり、パフォーマンスの良い)方法がありますが、時間に敏感なループで多くの呼び出しを行わない場合は、これが最も簡単な方法ですが、少し型が安全ではありません。
factの関数は何度でも呼び出せることに注意してください。Scriptは状態を保持し、何度でも実行できる状態になっています。
** DynValueのすべて [#w1153ae3]
- すべてはDynValueであり、DynValueである
DynValueの概念はMoonSharpの根幹にあり、これまではあまり触れずに済んでいましたが、実際にはこの話題に触れずに先へ進むのは難しいでしょう。
タイトルにもあるように、MoonSharpの(ほぼ)すべてがDynValueオブジェクトのインスタンスです。
DynValueは、スクリプト内の値を表し、その型に関わらず、テーブル、関数、数値、文字列などすべてを表現できます。
では、まず前回のチュートリアルの最後のステップから始めて、代わりにDynValue(s)を使用するように変更してみましょう。
*** ステップ1: 前回のチュートリアルで行ったことをすべてやり直す [#f240d083]
再び、前回の最後のステップから始めます。やったよね?
リファレンスドキュメントはここで入手できます。
*** ステップ2: fact関数をDynValueで取得する [#j049e11e]
最初の変更点として、fact変数を別の方法で取得します。
double MoonSharpFactorial()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end";
Script script = new Script();
script.DoString(scriptCode);
DynValue luaFactFunction = script.Globals.Get("fact");
DynValue res = script.Call(luaFactFunction, 4);
return res.Number;
}
この行について少し説明しましょう。
DynValue luaFactFunction = script.Globals.Get("fact");
これは、スクリプトのグローバルからfact関数を取得します。
Getメソッドは、インデクサープロパティとは反対に、DynValueを返します。
インデクサーは、代わりにSystem.Objectに変換しようとします。
この場合、DynValueに関心があるので、Getメソッドを使用します。
*** ステップ3: 数値をDynValueで取得する [#afda98b8]
さて、もう1つの変換を避けることにも関心があるなら、Callメソッドが親切に提供する暗黙の変換を使用する代わりに、直接数値を渡すこともできます。
ここでは必要ありませんが、他の場所では必要になるかもしれません。すべてがあなたのために魔法のような変換をしてくれるわけではありません!
double MoonSharpFactorial2()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n*fact(n - 1)
end
end";
Script script = new Script();
script.DoString(scriptCode);
DynValue luaFactFunction = script.Globals.Get("fact");
DynValue res = script.Call(luaFactFunction, DynValue.NewNumber(4));
return res.Number;
}
つまり、数値を以下のように置き換えました。
DynValue.NewNumber(4)
DynValueには、"New"で始まる多くのファクトリメソッドがあります(NewString、NewNumber、NewBooleanなど)。
これらを使用して、最初から値を作成できます。
また、便利なFromObjectメソッドもあります。これはオブジェクトからDynValueを作成するもので、Callが内部で使用していたものです。
*** ステップ4: DataTypeの理解 [#vbad2568]
DynValueで最も重要なプロパティの1つはTypeです。
Typeプロパティは列挙型で、DynValueにどのような種類のデータが含まれているかを示します。
DynValueに何が含まれているかを知りたい場合は、いつでもTypeプロパティを照会できます。
// 新しい数値を作成
DynValue v1 = DynValue.NewNumber(1);
// 新しい文字列を作成
DynValue v2 = DynValue.NewString("ciao");
// 自動変換を使用して別の文字列を作成
DynValue v3 = DynValue.FromObject(new Script(), "hello");
// Number - String - String と表示される
Console.WriteLine("{0} - {1} - {2}", v1.Type, v2.Type, v3.Type);
// Number - String - 0になると期待すべきではないゴミの数値が表示される
Console.WriteLine("{0} - {1} - {2}", v1.Number, v2.String, v3.Number);
DynValueの一部のプロパティは、値が特定の型の場合にのみ意味のある値を持つことに注意してください。
例えば、NumberプロパティはDataType.Numberの場合にのみ意味のある値を持つことが保証されており、Stringプロパティも同様です。
DynValueの中身が確実にわかっていない限り、そのプロパティを使用する前に必ずDynValueのTypeを確認するようにしてください。
DynValueが特定の型を含んでいると想定しているのに、別の型を含んでいると、間違いやエラーの一般的な原因になります。
*** ステップ5: タプル [#gddb0564]
ご存知のように(あるいは知っておくべきですが)、Luaでは関数から複数の値を返すことができます(その他の状況でも)。
これを処理するために、特別なDynValue型(DataType.Tuple)が使用されます。タプルにはTupleプロパティがあり、これはDynValueオブジェクトの配列で、タプルのメンバーです。
これは聞こえるよりも簡単です。
DynValue ret = Script.RunString("return true, 'ciao', 2*3");
// "Tuple" と表示される
Console.WriteLine("{0}", ret.Type);
// 以下が表示される:
// Boolean = true
// String = "ciao"
// Number = 6
for (int i = 0; i < ret.Tuple.Length; i++)
Console.WriteLine("{0} = {1}", ret.Tuple[i].Type, ret.Tuple[i]);
終わりに
DynValuesについて終わりです。
まだまだ学ぶべきことはたくさんありますが、このトピックを始めるにはこれで十分でしょう。
すべてがこれを中心に回っているので、これを肝に銘じておいてください。
** C#をコールバックする [#o1bf9b18]
- ..そしてF#、VB.NET、C++/CLI、Booなども
スクリプトは、アプリケーション自体に実装された構成要素から始まるビジネスロジックをカスタマイズできるため、アプリケーションで役立ちます。
ビジネスアプリケーション、ビデオゲーム、またはある種のツールにスクリプトを組み込む場合、最初の関心事はスクリプトとアプリケーションの相互運用性と、オブジェクトの(ある意味での)共有です。
そして、手続き型プログラミングの基本的な構成要素は関数です。
*** 演習1、ステップ1: 階乗に飽きるな [#g99c9a42]
これまで使ってきた標準的な階乗スクリプトで基礎を押さえましょう。
private static double CallbackTest()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return n * fact(n - 1);
end
end";
Script script = new Script();
script.DoString(scriptCode);
DynValue res = script.Call(script.Globals["fact"], 4);
return res.Number;
}
*** 演習1、ステップ2: 乗算関数をカスタマイズする [#o017308d]
さて、乗算をホストアプリケーションのAPI関数で実装したいとしましょう。
この場合、そうする目的はありませんが、学ぶためにここにいるので、それが良いアイデアだと仮定しましょう。
private static int Mul(int a, int b)
{
return a * b;
}
private static double CallbackTest()
{
string scriptCode = @"
-- 階乗関数を定義
function fact (n)
if (n == 0) then
return 1
else
return Mul(n, fact(n - 1));
end
end";
Script script = new Script();
script.Globals["Mul"] = (Func<int, int, int>)Mul;
script.DoString(scriptCode);
DynValue res = script.Call(script.Globals["fact"], 4);
return res.Number;
}
それだけです!
script.Globals["Mul"] = (Func<int, int, int>)Mul;
これにより、グローバル環境のMul変数が、アプリケーションのMulデリゲートを指すように設定されます。
ここでは、C#コンパイラを満足させるために、デリゲートをFunc<int, int, int>型にキャストしています。
デリゲートをSystem.Objectにキャストするために使用する方法は何でも構いません。
また、メソッドをstaticに定義しましたが、この場合、それらがstaticである必要は全くありません。
インスタンスメソッドも問題ありません。
*** 演習2: 数値のシーケンスを返す [#ved358ee]
問題: 整数のシーケンスを返すAPI関数を用意する。スクリプトは受け取った数値を合計して、その合計値を返す。
private static IEnumerable<int> GetNumbers()
{
for (int i = 1; i <= 10; i++)
yield return i;
}
private static double EnumerableTest()
{
string scriptCode = @"
total = 0;
for i in getNumbers() do
total = total + i;
end
return total;
";
Script script = new Script();
script.Globals["getNumbers"] = (Func<IEnumerable<int>>)GetNumbers;
DynValue res = script.DoString(scriptCode);
return res.Number;
}
ここでも、特に難しいことはありません。
IEnumerable(またはIEnumerator)が、スクリプトを実行するためにLuaイテレータに自動的に変換される様子がわかります。
スクリプトには直接実行可能なコードが含まれているため、Call .. callを必要とせずに、DoString時にgetNumbersメソッドにアクセスしていることにも注目してください。
これを覚えておいてください。
DoStringとDoFileは、スクリプトに含まれるコードを即座に実行します!
注意すべき点が1つあります。MoonSharpは、System.Collections.IEnumerableとSystem.Collections.IEnumeratorの型のイテレータを変換できます。
つまり、ジェネリックではないバリアントです。
何らかの理由でジェネリックイテレータを実装する際にジェネリック以外のイテレータを実装しない場合、そのイテレータは機能しません。
上記のように、すべての標準コレクション型とイテレータメソッドは、デフォルトでジェネリック以外のバリアントを実装しているので、あまり心配する必要はありません。
*** テーブルを返す [#me086187]
問題: 今度は整数のシーケンスをテーブルで返すAPI関数を用意する。
private static List<int> GetNumberList()
{
List<int> lst = new List<int>();
for (int i = 1; i <= 10; i++)
lst.Add(i);
return lst;
}
private static double TableTest1()
{
string scriptCode = @"
total = 0;
tbl = getNumbers()
for _, i in ipairs(tbl) do
total = total + i;
end
return total;
";
Script script = new Script();
script.Globals["getNumbers"] = (Func<List<int>>)GetNumberList;
DynValue res = script.DoString(scriptCode);
return res.Number;
}
ここでは、List<int>がLuaテーブルに自動的に変換される様子がわかります!
結果のテーブルは、Luaテーブルが通常そうであるように、1から始まるインデックスを持つことに注意してください。
ただし、もっと良い方法があります。
関数内で直接Luaテーブルを構築することができます。
private static Table GetNumberTable(Script script)
{
Table tbl = new Table(script);
for (int i = 1; i <= 10; i++)
tbl[i] = i;
return tbl;
}
private static double TableTest2()
{
string scriptCode = @"
total = 0;
tbl = getNumbers()
for _, i in ipairs(tbl) do
total = total + i;
end
return total;
";
Script script = new Script();
script.Globals["getNumbers"] = (Func<Script, Table>)(GetNumberTable);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
Tableオブジェクトを使用してテーブルを操作するのがいかに簡単かがわかります。
ただし、注意すべき点が2つあります。
新しいTableオブジェクトを作成するには、実行中のスクリプトへの参照が必要です。
CLR関数にScriptパラメータがあり、それがLuaスクリプトで利用可能な場合、MoonSharpはそれを自動的に設定します。
これは、(このように使用される可能性は低いですが)ScriptExecutionContextやCallbackArgumentsの型でも発生します。
これらが何をするのかわからなくても心配しないでください。
MoonSharpの基本を理解するのに必要ありません!
良い習慣として、可能な限りScriptオブジェクトを保持するようにしてください。
(テーブルの作成など)Scriptオブジェクトを使用してのみ実行できることがいくつかあります。
*** テーブルを受け入れる [#p43434d6]
テーブルは自動的にList<T>に変換されます。
例えば、
public static double TableTestReverse()
{
string scriptCode = @"
return dosum { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
";
Script script = new Script();
script.Globals["dosum"] = (Func<List<int>, int>)(l => l.Sum());
DynValue res = script.DoString(scriptCode);
return res.Number;
}
ただし、これによって一部のプラットフォーム(具体的にはiOS)で問題が発生する可能性があります。
この問題を回避する方法はたくさんありますが(他のチュートリアルで見ることができます)、次の方法では問題なく機能し、さらに高速であると言えます。
public static double TableTestReverseSafer()
{
string scriptCode = @"
return dosum { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
";
Script script = new Script();
script.Globals["dosum"] = (Func<List<object>, int>)(l => l.OfType<int>().Sum());
DynValue res = script.DoString(scriptCode);
return res.Number;
}
さらに高速な別の方法は、Tableオブジェクトを使用することです。
static double Sum(Table t)
{
var nums = from v in t.Values
where v.Type == DataType.Number
select v.Number;
return nums.Sum();
}
private static double TableTestReverseWithTable()
{
string scriptCode = @"
return dosum { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }
";
Script script = new Script();
script.Globals["dosum"] = (Func<Table, double>)Sum;
DynValue res = script.DoString(scriptCode);
return res.Number;
}
しかし、ここではDynValue(s)を扱う必要があります。
これらすべてを理解するには、MoonSharpがLuaの型をC#の型にどのようにマッピングするか、その逆はどうかについて、もう少し深く掘り下げる必要があります。
次の部分で説明します。
** 自動変換の説明 [#zdd8f29e]
- ..今回はコードなし
MoonSharpとCLRの統合についてさらに掘り下げる前に、型がどのように相互にマッピングされるかを明確にする必要があります。
残念ながら、逆方向は順方向とは大きく異なるので、2つを別々に分析します。
これは少し複雑すぎませんか? - あなたはそう尋ねるかもしれません。
確かにそうです。
自動的なものは良いですが、失敗すると、ひどく複雑な方法で失敗します。疑問がある場合や、物事が複雑すぎる場合は、物事を単純化する必要があります。
2つの方法があります:DynValueをそのまま使用するか、カスタムコンバーターを使用するかです。
これらの解決策は、正気と単純さを得るだけでなく、自動変換よりもかなり高速です!
*** カスタムコンバーター [#iaa65800]
変換プロセスをカスタマイズすることは可能ですが、設定はグローバルで、すべてのスクリプトに影響します。
変換をカスタマイズするには、
Script.GlobalOptions.CustomConverters
の適切なコールバックを設定するだけです。
例えば、CLRからスクリプトへの変換が行われるときに、すべての
StringBuilder
オブジェクトを大文字の文字列に変換したい場合は、次のようにします。
Script.GlobalOptions.CustomConverters.SetClrToScriptCustomConversion<StringBuilder>(
v => DynValue.NewString(v.ToString().ToUpper()));
テーブルがIList<int>に一致する必要がある場合に、テーブルの変換方法をカスタマイズしたい場合は、次のように記述できます。
Script.GlobalOptions.CustomConverters.SetScriptToClrCustomConversion(DataType.Table, typeof(IList<int>),
v => new List<int>() { ... });
コンバーターがnullを返した場合、システムはカスタムコンバーターが存在しないかのように動作し、自動変換を試みます。
*** CLR型からMoonSharp型への自動変換 [#y6c7cab4]
この変換は、次の状況で適用されます。
- スクリプトによって呼び出された関数からオブジェクトを返すとき
- ユーザーデータのプロパティからオブジェクトを返すとき
- インデックス演算子を使用してテーブルに値を設定するとき
- DynValue.FromObjectを呼び出すとき
- DynValueの代わりにSystem.Objectを取る関数のオーバーロードを使用するとき
この変換は実際にはかなり簡単です。次の表は、変換を説明しています。
| CLR型 | C#のわかりやすい名前 | Lua型 | 備考 |
|----------------------------------------|---------------------|----------|--------------------------------------------------------------|
| void | | (no value) | これはメソッドの戻り値にのみ適用できます。 |
| null | | nil | nullはすべてnilに変換されます。 |
| MoonSharp.Interpreter.DynValue | | * | DynValueはそのまま渡されます。 |
| System.SByte | sbyte | number | |
| System.Byte | byte | number | |
| System.Int16 | short | number | |
| System.UInt16 | ushort | number | |
| System.Int32 | int | number | |
| System.UInt32 | uint | number | |
| System.Int64 | long | number | 変換により、暗黙的に精度が失われる可能性があります。 |
| System.UInt64 | ulong | number | 変換により、暗黙的に精度が失われる可能性があります。 |
| System.Single | float | number | |
| System.Decimal | decimal | number | 変換により、暗黙的に精度が失われる可能性があります。 |
| System.Double | double | number | |
| System.Boolean | bool | boolean | |
| System.String | string | string | |
| System.Text.StringBuilder | | string | |
| System.Char | char | string | |
| MoonSharp.Interpreter.Table | | table | |
| MoonSharp.Interpreter.CallbackFunction | | function | |
| System.Delegate | | function | |
| System.Object | object | userdata | 型がuserdataに登録されている場合のみ。 |
| System.Type | | userdata | 型がuserdataに登録されている場合のみ。静的メンバーアクセス。 |
| MoonSharp.Interpreter.Closure | | function | |
| System.Reflection.MethodInfo | | function | |
| System.Collections.IList | | table | 結果のテーブルは1からインデックスが付けられます。すべての値はこれらのルールを使用して変換されます。 |
| System.Collections.IDictionary | | table | すべてのキーと値はこれらのルールを使用して変換されます。 |
| System.Collections.IEnumerable | | iterator | すべての値はこれらのルールを使用して変換されます。 |
| System.Collections.IEnumerator | | iterator | すべての値はこれらのルールを使用して変換されます。 |
これには、コレクションのかなり良いカバレッジが含まれています。ほとんどのコレクションは、IList、IDictionary、IEnumerable、IEnumeratorのいずれかを実装しているためです。
独自のコレクションを書く場合は、これらの非ジェネリックインターフェイスの1つを実装することを忘れないでください。
このロジックを使用して変換できない値はすべて、ScriptRuntimeExceptionをスローします。
*** MoonSharp型からCLR型への標準の自動変換 [#b443915a]
反対の変換はかなり複雑です。
実際、2つの異なる変換パスがあります。
「標準」の変換パスと、「制約付き」の変換パスです。
最初の変換パスは、実際に何を受け取りたいかを指定せずにDynValueをオブジェクトに変換するように要求するたびに適用され、もう一方の変換パスは、一致させるSystem.Typeがある場合に適用されます。
これは以下の場合に使用されます。
- DynValue.ToObjectを呼び出すとき
- インデクサーを使用してテーブルから値を取得するとき
- 制約付き変換の特定のサブケース(下記参照)
ここでは、デフォルトの変換を見ていきます。実際には簡単です。
| MoonSharp型 | CLR型 | 備考 |
|----------------------------------------|----------------------------------------|---------------------------------------------------------------|
| nil | null | nilであるすべての値に適用されます。 |
| boolean | System.Boolean | |
| number | System.Double | |
| string | System.String | |
| function | MoonSharp.Interpreter.Closure | DataTypeがFunctionの場合。 |
| function | MoonSharp.Interpreter.CallbackFunction | DataTypeがClrFunctionの場合。 |
| table | MoonSharp.Interpreter.Table | |
| tuple | MoonSharp.Interpreter.DynValue[] | |
| userdata | (special) | userdataに格納されているオブジェクトを返します。「静的」userdataの場合は、Typeが返されます。 |
このロジックを使用して変換できない値はすべて、ScriptRuntimeExceptionをスローします。
*** MoonSharp型からCLR型への制約付き自動変換 [#b3fb4110]
しかし、制約付き自動変換ははるかに複雑です。この場合、MoonSharpの値は、特定の型のCLR変数に格納する必要があり、これらの変換を行う方法はほぼ無限にあります。
これは以下の場合に使用されます。
- DynValue.ToObject<T>を呼び出すとき
- CLR関数呼び出しやプロパティ設定でスクリプト値をパラメーターに変換するとき
MoonSharpは値の変換に非常に努力しますが、特にテーブルが関与する場合は、変換には確実に限界があります。
この場合、変換は単純なマッピングのテーブルではなく、プロセスに近いので、ターゲットの型を1つずつ分析していきましょう。
*** 特殊なケース [#h25b1ae1]
- ターゲットがMoonSharp.Interpreter.DynValue型の場合は変換されず、元の値が返されます。
- ターゲットがSystem.Object型の場合は、前述のデフォルトの変換が適用されます。
- マッピングされるnil値の場合、nullは参照型とnullable値型にマッピングされ、一部のケース(たとえば、デフォルトが指定されている関数呼び出しなど)では、non-nullable値型にデフォルト値に一致させる試行が行われます。それ以外の場合は例外がスローされます。
- 値が提供されていない場合は、可能であればデフォルト値が使用されます。
*** 文字列 [#gde47277]
文字列は、System.String、System.Text.StringBuilder、System.Charに自動的に変換できます。
*** ブール値 [#lb1bcc8c]
ブール値は、System.BooleanやSystem.Nullable<System.Boolean>に自動的に変換できます。また、System.String、System.Text.StringBuilder、System.Charにも変換できます。
*** 数値 [#dc22c32c]
数値は、System.SByte、System.Byte、System.Int16、System.UInt16、System.Int32、System.UInt32、System.Int64、System.UInt64、System.Single、System.Decimal、System.Doubleとそれらのnullableバージョンに自動的に変換できます。また、System.String、System.Text.StringBuilder、System.Charにも変換できます。
*** 関数 [#z9738d47]
スクリプト関数は、MoonSharp.Interpreter.ClosureまたはMoonSharp.Interpreter.ScriptFunctionDelegateに変換されます。コールバック関数は、MoonSharp.Interpreter.ClrFunctionまたはSystem.Func<ScriptExecutionContext, CallbackArguments, DynValue>に変換されます。
*** ユーザーデータ [#jbe4344d]
ユーザーデータは、「静的」でない場合にのみ変換されます(チュートリアルのuserdataセクションを参照)。変換対象のオブジェクトを割り当てることができる型の場合に変換されます。
また、オブジェクトのToString()メソッドを呼び出すことで、System.String、System.Text.StringBuilder、System.Charにも変換できます。
*** テーブル [#g19e08d9]
テーブルは以下に変換できます。
- MoonSharp.Interpreter.Table - もちろん
- Dictionary<DynValue, DynValue>から割り当て可能な型。
- Dictionary<object, object>から割り当て可能な型。キーと値は、デフォルトのマッピングを使用してマッピングされます。
- List<DynValue>から割り当て可能な型。
- List<object>から割り当て可能な型。要素は、デフォルトのマッピングを使用してマッピングされます。
- DynValue[]から割り当て可能な型。
- object[]から割り当て可能な型。要素は、デフォルトのマッピングを使用してマッピングされます。
- T[]、IList<T>、List<T>、ICollection<T>、IEnumerable<T>。ここで、Tは変換可能な型です(他のリストなどを含む)。
- IDictionary<K,V>、Dictionary<K,V>。ここで、KとVは変換可能な型です(他のリストなどを含む)。
ジェネリックや型付き配列への変換には、次の制限があることに注意してください。
- リフレクションを使用して実行時に型が作成されるため、速度が遅くなります。
- アイテムの型が値型の場合、iOSやAOTモードで実行される他のプラットフォームとの非互換性を引き起こす可能性があります。事前にテストを行ってください。
これらの問題を補うために、開発の後半でカスタムコンバーターを追加することもできます!
一部の変換(List<int>への変換など)は、Unity/iOSなどのAOTプラットフォームで問題を引き起こす可能性があります。この種の問題を回避するには、カスタムコンバーターを登録してください。
iOSなどのAOTプラットフォームをターゲットにする場合は、値型のジェネリックには常に注意してください。参照型のジェネリックは、一般的に問題を引き起こしません。
** オブジェクトの共有 [#obbc6f3a]
- LuaとC#はお互いに話す
注:このページに記載されている一部の機能は、masterブランチの現在の状態を反映しています(つまり、最新のリリースには存在しない機能かもしれません)。
MoonSharpの便利な機能の1つは、.NETオブジェクトをスクリプトと共有できることです。
デフォルトでは、型はすべてのパブリックメソッド、パブリックプロパティ、パブリックイベント、パブリックフィールドをLuaスクリプトと共有します。
MoonSharpVisibleアトリビュートを使用して、このデフォルトの可視性をオーバーライドできます。
CLRコードとスクリプトコード間のインターフェイスとして専用のオブジェクトを使用することをお勧めします(アプリケーションの内部モデルをスクリプトに公開するのとは対照的です)。
多くのデザインパターン(アダプター、ファサード、プロキシなど)が、そのようなインターフェイス層の設計に役立ちます。
これは特に以下のために重要です。
- スクリプトができること、できないことを制限する(セキュリティ!modの作成者にエンドユーザーの個人ファイルを削除する方法を見つけてほしいですか?)
- スクリプト作成者にとって意味のあるインターフェイスを提供する
- インターフェイスを別々に文書化する
- スクリプトを破ることなく、内部ロジックとモデルを変更できるようにする
これらの理由から、MoonSharpはデフォルトで、スクリプトで使用可能な型の明示的な登録を必要とします。
スクリプトを信頼できるシナリオでは、`
UserData.RegistrationPolicy = InteropRegistrationPolicy.Automatic;
を使用して自動登録をグローバルに有効にできます。
危険なので、警告しておきます。
*** メニューにあるもの [#dda0ae94]
では、メニューにあるものを見てみましょう。
- まず、型記述子について説明しましょう - 舞台裏で何が起こっているのか、そしてインターオペラビリティシステム全体をどのようにオーバーライドできるかを説明する少しの理論
- シンプルに保つ - 始めるための最も簡単な方法
- 少し複雑 - 少し複雑さと詳細を加えて掘り下げる
- 静的メンバーの呼び出し - 静的メンバーを呼び出す方法
- ':' と '.' のどちらを使うべきか - メソッドの呼び出し方に関する簡単な質問
- オーバーロード - オーバーロードがどのように処理されるか
- ByRefパラメーター(C#のref/out) - ref/outパラメーターがどのように処理されるか
- インデクサー - インデクサーの処理方法
- ユーザーデータ上の演算子とメタメソッド - 演算子のオーバーロード方法など
- 拡張メソッド - 拡張メソッドの使用方法
- イベント - イベントの使用方法
- インターオペラビリティアクセスモード - インターオペラビリティアクセスモードとその動作
- MoonSharpHiddenとMoonSharpVisibleによる可視性の変更 - メンバーの可視性をオーバーライドする方法
- メンバーの削除 - メンバーの可視性を削除する方法
たくさんありますね。では、始めましょう。
*** 型記述子 [#ce23a6ed]
まず、型記述子について説明しましょう
最初に、インターオペラビリティがどのように実装されているかについて少し理論を説明します。
すべてのCLR型は、CLR型をスクリプトに記述する役割を持つ「型記述子」にラップされます。
型をインターオペラビリティ用に登録するとは、型を記述子(MoonSharpが自身で作成できる)に関連付けることを意味します。
この記述子は、メソッド、プロパティなどのディスパッチに使用されます。
次のセクション以降では、MoonSharpが提供する「自動」記述子について説明しますが、速度、機能の追加、セキュリティなどのために独自の記述子を実装することもできます。
*** 独自の記述子を実装したい場合 [#i28f4d6d]
独自の記述子を実装したい場合(簡単ではなく、必要でない限り行うべきではありません)、次のような方法があります。
- 独自の型を記述するためのアドホックなIUserDataDescriptorを作成する - これが最も難しい方法です
- 型にIUserDataTypeインターフェイスを実装させる。これは簡単ですが、オブジェクトのインスタンスなしで静的メンバーを処理できないことを意味します。
- StandardUserDataDescriptorを拡張または埋め込み、必要な側面を変更しながら、残りの動作を維持する。
記述子の作成を助けるために、次のクラスが利用できます。
- StandardUserDataDescriptor - これはMoonSharpが実装する型記述子です
- StandardUserDataMethodDescriptor - これは単一のメソッド/関数の記述子です
- StandardUserDataOverloadedMethodDescriptor - これはオーバーロードされたメソッドや拡張メソッドの記述子です
- StandardUserDataPropertyDescriptor - これは単一のプロパティの記述子です
- StandardUserDataFieldDescriptor - これは単一のフィールドの記述子です
*** ユーザーデータとしての値型とのインターオペラビリティに関する注意事項。 [#jcae2115]
値型をパラメーターとして渡す関数を呼び出す場合と同様に、スクリプトはユーザーデータのコピーを操作するため、たとえば、ユーザーデータのフィールドを変更しても、元の値には反映されません。
繰り返しになりますが、これは値型の標準的な動作と変わりませんが、人を驚かせるのに十分です。
さらに、値型は参照型ほどの最適化の範囲をサポートしていないため、値型での一部の操作は参照型よりも遅くなる可能性があります。
*** シンプルに保つ [#b3bac791]
では、最初の例を見てみましょう。
[MoonSharpUserData]
class MyClass
{
public double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
double CallMyClass1()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// MoonSharpUserData型をすべて自動的に登録
UserData.RegisterAssembly();
Script script = new Script();
// MyClassのインスタンスをスクリプトのグローバルに渡す
script.Globals["obj"] = new MyClass();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
ここでは以下のことを行っています。
- [MoonSharpUserData]アトリビュートを持つクラスを定義
- MyClassオブジェクトのインスタンスをスクリプトのグローバルとして渡す
- スクリプトからMyClassのメソッドを呼び出す。コールバックのマッピングルールがすべて適用されます
*** 少し複雑 [#x8b60ac6]
もう少し複雑な例を試してみましょう。
class MyClass
{
public double CalcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
static double CallMyClass2()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// MyClassだけを明示的に登録
UserData.RegisterType<MyClass>();
Script script = new Script();
// ユーザーデータを明示的に作成
DynValue obj = UserData.Create(new MyClass());
script.Globals.Set("obj", obj);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
*** 大きな違い [#a756817e]
大きな違いは以下の通りです。
- [MoonSharpUserData]アトリビュートはありません。もう必要ありません。
- RegisterAssemblyの代わりに、RegisterTypeを呼び出して特定の型を登録します。
- ユーザーデータのDynValueを明示的に作成します。
また、C#コードではメソッドがCalcHypotenuse と呼ばれていましたが、LuaスクリプトではcalcHypotenuse として呼ばれていることに注目してください。
他のバージョンが存在しない限り、MoonSharpは異なる言語の構文規則間の一貫性を高めるために、いくつかの限定的な方法でケースを自動的に調整してメンバーを一致させます。たとえば、SomeMethodWithLongNameというメンバーは、LuaスクリプトからsomeMethodWithLongNameやsome_method_with_long_nameとしてアクセスすることもできます。
*** 静的メンバーの呼び出し [#b88a2ae1]
calcHypotenuse メソッドが静的であるとしましょう。
[MoonSharpUserData]
class MyClassStatic
{
public static double calcHypotenuse(double a, double b)
{
return Math.Sqrt(a * a + b * b);
}
}
*** 呼び出す方法 [#x240cc3a]
これを呼び出すには2つの方法があります。
*** 方法1 - 静的メソッドはインスタンスから透過的に呼び出すことができます - 何もする必要はなく、すべて自動的に行われます [#ecac98ea]
double MyClassStaticThroughInstance()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// MoonSharpUserData型をすべて自動的に登録
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = new MyClassStatic();
DynValue res = script.DoString(scriptCode);
return res.Number;
}
*** 別の方法 [#of33f4f5]
- 型を直接渡す(またはUserData.CreateStaticメソッドを使用する)ことで、プレースホルダーのユーザーデータを作成できます。
double MyClassStaticThroughPlaceholder()
{
string scriptCode = @"
return obj.calcHypotenuse(3, 4);
";
// MoonSharpUserData型をすべて自動的に登録
UserData.RegisterAssembly();
Script script = new Script();
script.Globals["obj"] = typeof(MyClassStatic);
DynValue res = script.DoString(scriptCode);
return res.Number;
}
':' と '.' のどちらを使うべきか
上記の例のコードを考えると、この構文を使うべきでしょうか
return obj.calcHypotenuse(3, 4);
それともこちらでしょうか?
return obj:calcHypotenuse(3, 4);
99.999%の場合、違いはありません。MoonSharpは、ユーザーデータに対して呼び出しが行われていることを認識し、それに応じて動作します。
*** 違いが出る可能性があるコーナーケース [#r13135cc]
違いが出る可能性があるコーナーケースがあります。たとえば、プロパティがデリゲートを返し、そのデリゲートをインスタンスとして元のオブジェクトですぐに呼び出す場合などです。これはまれなシナリオですが、そのような場合は手動で処理する必要があります。
*** オーバーロード [#v6fea100]
オーバーロードされたメソッドがサポートされています。オーバーロードされたメソッドのディスパッチは、いくぶん黒魔術的で、C#のオーバーロードディスパッチほど決定的ではありません。
これは、いくつかの曖昧さが存在するためです。たとえば、オブジェクトはこれらの2つのメソッドを宣言できます。
void DoSomething(int i) { ... }
void DoSomething(float f) { ... }
Luaのすべての数値がdoubleであることを考えると、MoonSharpはどのメソッドをディスパッチすべきかをどのように知ることができるでしょうか?
この問題を解決するために、MoonSharpは入力型に対するすべてのオーバーロードのヒューリスティック係数を計算し、最適なオーバーロードを選択します。MoonSharpがオーバーロードを間違った方法で解決していると思われる場合は、フォーラムやdiscordに報告して、ヒューリスティックを調整してください。
MoonSharpは、ヒューリスティックの重みを可能な限り安定させようとしており、メソッド間のスコアが同点の場合は、常に決定的に同じものを選択します(ビルドやプラットフォーム間で一貫した動作を提供するため)。
とは言え、MoonSharpが考えているのとは異なるオーバーロードを選択することは十分にありえます。
そのため、オーバーロードが同等の処理を行うようにすることが非常に重要です。これは、間違ったオーバーロードを呼び出すことの影響を最小限に抑えるためです。これは、ベストプラクティスであるべきですが、ここでその概念を強化する価値があります。
- ByRefパラメーター(C#のref/out)
- ByRefメソッドパラメーター
* 参考 [#l6580144]
** MoonSharpによるUnityとLuaの連携 [#r91a70b3]
https://qiita.com/massu2357/items/425cc93b5965ceea29aa