graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

UnityScriptとDLL周りのあれこれ。

DLLにするとUnity上でリビルドがかからないから速くていいとか、実際どうなん?みたいなところ。

github.com

Mono.CSharp.Evaluator

いわゆる「eval」と呼ばれるもの。(特に初期化周りに)クセがあるけど、いわゆる REPL として使えるようにしておくとなかなか便利。

初期化

  • エディタ本体の初期化が終わるまで、Evaluatorの初期化は行わない
  • Evaluator.Init() は呼ばない
  • AppDomain.CurrentDomain.GetAssemblies() の初回呼び出しは必ず失敗する(らしい)

Evaluator の初期化時に参照先の設定をする必要があるんだけど、1つずつ指定するのは面倒だし事故の元なので、エディタの依存先を丸ごと設定したい。なので、この初期化はエディタ自体の初期化が完了したタイミングで行う必要がある。つまり Awake とかじゃなくて、初回 Update 時に行うのが安全。

あとはもう完全にバッドノウハウってかんじであれなんだけど、Evaluator 自体のバグ回避のためのあれこれ。

[InitializeOnLoadMethod]
public static void Initialize() {
    EditorApplication.update += Update;
}

private static void Update() {
    if (EditorApplication.isCompiling) {
        return;
    }

    // http://forum.unity3d.com/threads/mono-csharp-evaluator.102162/
    //Evaluator.Init(new string[] { });
    for (var i = 0; i < 2; ++i) {
        foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
            if (assembly != null) {
                Evaluator.ReferenceAssembly(assembly);
            }
        }
        Evaluator.Evaluate("null;");
    }
    Evaluator.Run("using UnityEngine; using UnityEditor; using System.Linq;");

    EditorApplication.update -= Update;
}

式の評価

こっちは特に面倒なこともなく、Evaluator.Evaluate() にソースコード文字列を渡すだけ。

object result;
bool isResultSet;
Evaluator.Evaluate(sourceCode.ToString(), out result, out isResultSet);

あとは、using をそのまま eval できると便利なので、そのへんは手動でハンドリングしてあげるといいかも。

var sourceCode = new StringBuilder();
foreach (var line in _sourceCode.Split('\n').Select(x => x.Trim())) {
    if (line.StartsWith("using")) {
        Evaluator.Run(line);
    } else {
        sourceCode.AppendLine(line);
    }
}
sourceCode.Append(";");

あとは、ソースコードの末尾にセミコロンを付けてあげるとか、eval の結果が Enumerable だったら展開してあげるとか。

Unityへの組み込み案

……というのを、なんか適当に TextArea あたりから拾ってあげれば十分かと。

f:id:hal1932:20161127021219j:plain

smcs

Unity 用の DLL を単体でビルドするやつ。

プロセス呼び出し

ぐぐればいくらでも情報が出て来るんだけど、まぁふつうに外部プロセスとして叩けばいい。場合によっては外部参照の追加なんかが必要だろうし、そのあたりは適当にプリプロセスを追加するかんじで。

using (var process = Process.Start(new ProcessStartInfo()
{
#if UNITY_EDITOR_WIN
    FileName = EditorUtil.Preference.MonoDirectory + "/bin/smcs.bat",
#elif UNITY_EDITOR_OSX
    FileName = EditorUtil.Preference.MonoDirectory + "/bin/smcs",
#endif
    Arguments = string.Join(
        " ",
        new[]
        {
            string.Format("-r:\"{0}\"", AssetUtil.CombinePath(ProjectInfo.UnityAssemblyRoot, "UnityEngine.dll")),
            string.Format("-r:\"{0}\"", AssetUtil.CombinePath(ProjectInfo.UnityAssemblyRoot, "UnityEditor.dll")),
            "-target:library",
            "-warnaserror+",
            string.Format("-out:\"{0}\"", outputPath),
            string.Join(" ", scripts.Select(x => string.Format("\"{0}\"", x)).ToArray()),
        }),
    CreateNoWindow = true,
    UseShellExecute = false,
    RedirectStandardOutput = true,
    RedirectStandardError = true,
}))

Unityへの組み込み案

「実装が固まったところからDLLにすればいいよ!」って、ぐぐるとたまに出てくるけど、実際そんなうまくいかないよね。「実装が固まる」とは……。というわけで、実装を変えたくなったらいつでもスクリプトに戻せて、キリのいいところでいつでもDLL化できる仕組みを考えてみる。

とはいえ、「DLLをスクリプトに戻す」といってもガチでディスアセンブルしたところで特にいいことはなくて、ILSpyとかでみるようなコードが出てきてもふつうに困る。ので、DLLにビルドする時点でのスクリプトのスナップショットをとっておいて、ディスアセンブルの代わりにスナップショットを元の位置に戻す方向で考えてみる。

New-Unity-Project/Composer.cs at master · hal1932/New-Unity-Project · GitHub

スクリプト → DLL

ここはふつうに、↑に挙げたようにプロセスを叩いてやる。ただし、あとで元に戻せるように、スナップショット用のディレクトリをつくって、ビルド結果のDLLと紐付けた上でスクリプトを退避しておく。

DLL → スクリプト

退避しておいたスナップショットを元の位置に戻して、DLLを消す。以上。