凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity で実行時に補完付きで REPL を行うプラグインの複数行対応をした

はじめに

以前公開した uREPL という Unity で実行時に C# のコードや予め登録したコマンドを実行するプラグインのアップデートをして、複数行編集に対応しました。

以前の記事はこちらです。

tips.hecomi.com

本エントリではちょっとした紹介と、備忘録として苦労した点を書いておきます。

デモ

ダウンロード

リリースページより unitypackage をダウンロードして展開した後、Hierarchy の Create > Create Other > uREPL で配置すれば使えます。テスト用の

コマンド

f:id:hecomi:20160817013536p:plain

以前の記事でも紹介しましたが、uREPL.Command アトリビュートstatic な関数につけることで任意のコマンドを引数付きで作成することが出来ます。型や引数名がわかるように表示を少し改善しました。

[uREPL.Command(name = "hoge", description = "hoge 1")]
static public void Hoge()
{
    uREPL.Log.Output("Hoge");
}

[uREPL.Command(name = "hoge", description = "hoge 2")]
static public void Hoge(string msg)
{
    uREPL.Log.Output(msg);
}

[uREPL.Command(name = "hoge", description = "hoge 2")]
static public void Hoge(string msg, int code)
{
    uREPL.Log.Output(msg + ": " + code);
}

f:id:hecomi:20160817014942p:plain

C# は型や引数の数など様々な情報を簡単に取得できるので、実行時に色々出来て面白いですね。コードはこのあたりです。

Mono.CSharp による補完

それに対して Mono.CSharp.Evaluator は基本的に文字列のリストとしてのみ補完のリストや宣言されている変数のリストを返してくるので、あまり拡張し甲斐がないのが寂しいですね...。また、そのままの入力を与えると余り補間してくれないので、複数行対応のためにいろいろとゴニョゴニョやる必要がありました。

GUI

今回一番苦労したのがこの複数行の GUI 対応です。

f:id:hecomi:20160816015445p:plain

Ctrl-M で一行編集モードと複数行編集モードを切り替えられます。コマンド入力エリア右上のアイコン(単数行時は −、 複数行時は ≡)をクリックしても切り替えられます。

UnityEngine.UIInput Field をカスタマイズして使用しているのですが、一行編集時と複数行編集時で色々と挙動を変えないとならなかった(Emacs ライクなキーバインディングを使う際に、Ctrl-P は一行編集時では前の履歴の表示、複数行編集時では上へ移動など)、結構時間を使ってしまいました。

Input Field

話に出てきたので Input Field のカスタマイズですが、これは以下のコードを直接見て整合が取れるように色々と試行錯誤しながらやっていく形になります。

例えば Emacs ライクなキーを作るには、該当のキー押下イベントを取得した後、それに対応するように caretPosition を移動する形になります。例えば以下の様な形になります。

bool isCtrlPressed = 
    Input.GetKey(KeyCode.LeftControl) ||
    Input.GetKey(KeyCode.RightControl);
if (isCtrlPressed && Input.GetKey(KeyCode.F)) {
    caretPosition = Mathf.Min(caretPosition + 1, text.Length);
}

これだけだと複数行編集時にカーソルが画面外に行きスクロールする際、スクロールが更新されずにエラーとなることが有ります。なので caretPosition を更新した後、protected なメンバとして公開されている UpdateLabel() を呼ぶことで、スクロールされるエリア範囲内のm_DrawStartm_DrawEnd、テキストをレンダリングするための TextGenerator の更新などが行われます。

TextGenerator

テキストをレンダリングするためのコンポーネントで、テキストを描画するコンポーネントのソースを覗いてみると大体ついています。

テキストの情報が色々と入っており、例えば指定された位置の文字の横幅や行の情報(どの文字の位置が行頭か等)が含まれています。例えば補完ウィンドウを表示する位置を指定するために指定した文字の Canvas 上の位置を取得したい、となったときに次のようなコードを書きました。

public Vector3 GetPositionBeforeCaret(int offsetLen)
{
    if (isFocused) {
        var characters = textComponent.cachedTextGenerator.characters;
        var len = m_CaretPosition - m_DrawStart;
        if (len > 0 && len < characters.Count) {
            var info = characters[len];
            var ppu  = textComponent.pixelsPerUnit;
            var x = info.cursorPos.x / ppu;
            var y = info.cursorPos.y / ppu;
            var z = 0f;
            var prefixWidth = 0f;
            for (int i = 0; i < offsetLen && i < len; ++i) {
                prefixWidth += characters[len - 1 - i].charWidth;
            }
            prefixWidth /= ppu;
            var inputTform = GetComponent<RectTransform>();
            return inputTform.localPosition + new Vector3(x - prefixWidth, y, z);
        }
    }
    return -9999f * Vector3.one;
}

おわりに

issue で HoloLens で使いたい、とのお話があったので頑張って対応しましたが、前のコードがあまり綺麗でなかったのもあり、修正が思ったより大変でした...。

現状の機能だけだと、コマンドはそこそこ便利ですが、コードの実行時の評価は特に便利ではなくておもちゃ程度にしかならないので...、もう少し開発の支援となるような機能を追加していきたいです。またしばらく更新意欲が高まるまで放置かもしれないですがちまちまとアップデートは続けていこうと思いますので、機能のご要望などありましたら教えて下さい。