📝

Unityでメモするツールとその中身について

2024/10/17に公開

作っているもの

UNote という Unity 内でメモするためのツールを作成しています。
記事公開時点のバージョンは 0.4.1 で、開発環境は Unity2022.3 + UI Toolkit です。
https://github.com/gok11/UNote

このツールではメモエディターからメモを書いたりメモを検索したり、

Inspector からでもそのアセットに関するメモを操作したり、

シーンビューやゲームビューのスクショを撮影し、その時点のカメラの座標も記録して後からカメラに適用しなおす、といったことができます。

なぜ作っているのか

開発しているといろいろとメモを取ってチーム内で共有したくなることがあります。
例えばマテリアルが剥がれるなどの不具合が発生しているのを見つけた場合や、あるアセットがどんな事情で今の仕様になったのか、ステージのチェックをしてフィードバックを返したい場合などなど。

それらは通常プロジェクト管理ツールやチャットツール上でスクリーンショットなどと共にメモされたりチーム内で共有されると思いますが、外部ツール→Unityだと素早くその情報にアクセスできなかったり、逆に Unity 内からアセット単体を見て何か注意事項などがあったか思い出そうとした場合は Unity 外で情報を探すことになります(しかも時間が経ってたりすると中々見つけられないことも)。

短期的な開発ならその状態でも困ることはないと思いますが開発が長期化したりチームの規模がある程度大きくなるとこの辺りのコストも無視できなくなることがあり、そうしたコストを下げることを目的に UNote を開発し始めました。

特徴と実装について

UNote の機能の中で特徴的だと思っているものとその実装についての紹介です。

メモに追加情報を設定

メモにはシンプルなテキスト以外にアセットの参照情報等を追加できるようにしました。

例えば編集中に次のように見えているテキストは、

編集を終えると次のような ObjectField として表示されます。

これ自体は独自のタグとともに GUID が記載されていれば ObjectField として追加、のような処理をリンク先にあるパーサーで地道にテキストの種類ごとに行っているだけです。
https://github.com/gok11/UNote/blob/b2b99dfceabf3d11a1055e0cfa6b873e13b35608/Assets/UNote/Editor/Scripts/Utils/NoteTextParser.cs

追加情報テキストはメモ入力エリア左下の + ボタンから追加できますが、毎回エクスプローラー等からファイルを指定するのは面倒なためメモ入力欄にアセットをドロップすることでも追加できるようにしました。

ただし、通常 TextField はドラッグ関係のイベントは受け付けないようになっているため何かしらの方法でドラッグ時の動作を上書きする必要があります。いくつか方法があると思いますが今回は TextField を覆う子要素を作成し、そちらにドラッグ関係のコールバックを登録する方法を採っています。
その部分のコードは次の通りで、DragAndDrop.visualMode = DragAndDropVisualMode.Generic; の行は見た目に関する設定かと思いきや必須で、 evt.StopPropagation(); は作法として書いていますが今回は必須ではありません。


        VisualElement dropAreaElement = new VisualElement();

        // ドラッグ中のカーソルがこの要素の上に重なった瞬間
        dropAreaElement.RegisterCallback<DragEnterEvent>(evt =>
        {
            DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            evt.StopPropagation();
        });

        // ドラッグ中のカーソルがこの要素の上で移動している間
        dropAreaElement.RegisterCallback<DragUpdatedEvent>(evt =>
        {
            DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
            evt.StopPropagation();
        });

        // この要素の上でドロップした瞬間
        dropAreaElement.RegisterCallback<DragPerformEvent>(evt =>
        {
            DragAndDrop.AcceptDrag();

            string[] paths = DragAndDrop.paths;
            foreach (var path in paths)
            {
                string guid = AssetDatabase.AssetPathToGUID(path);
                if (!string.IsNullOrEmpty(guid))
                {
                    if (!parentTextField.value.IsNullOrEmpty())
                    {
                        parentTextField.value += "\n";
                    }
                    parentTextField.value += $"[unatt-guid:{guid}]";
                }
            }
            
            evt.StopPropagation();
        });

今は DragPerformEvent ではアセットから GUID を得たいので paths を見ていますが、将来的にシーンのオブジェクトも扱えるようにする際は objectReferences を考慮する予定です。

スクショ撮影と編集機能

ゲームビュー、シーンビューの見た目を撮影してメモの追加データとして設定する機能を実装しています。また、自分で撮ったスクショは後から編集もできるようにしました。
スクショは RenderTexture に描きこんでから Texture2D に変換するよくある方式で、メモ内にそれを表示するのも先ほどの追加情報の表示と同様の仕組みのため割愛します。

編集については画像エディターを作成しました。エディター拡張で Undo 対応の画像のペイント編集をサポートするのはそれなりに珍しいのではないかと思います。

ソースコードはこちらです。
https://github.com/gok11/UNote/blob/main/Assets/UNote/Editor/Scripts/GUI/ImageEdit/ImageEditWindow.cs

中身の概要ですがスクショと同じ大きさの透明な画像を用意し、その画像に対してマウスの座標を元に SetPixel() で色を着けており、保存時には透明な画像を元画像に透明度を考慮しつつ合成という処理を行っています。SetPixel() で描いてパフォーマンスが出るか当初は心配でしたが、SetPixel() の度に Texture2D.Apply() を呼び出さなければ案外問題ありませんでした。
それより最初に実装してみて問題だったのは MouseMoveEvent の度に描き込む画像を対象として Undo.RecordObject() を呼び出していたことで、Undo は意図通りに動作したものの描きこみ中は毎フレーム Undo スタックの情報をそれなりに重い画像で置き換える状態になり明らかに重くなってしまいました。
対策として編集を終えた時点の画像を複製して List に格納しておきつつ、Undo に登録するのは画像の世代番号だけにして Undo.undoRedoPerformed 時に世代番号に対応する画像を読み出す現在の仕様に落ち着きました。

今後は UI をもう少し凝るのと消しゴムなど通常のペイントツールにもある要素に最低限対応していく予定です。

検索機能

こちらは機能の紹介のみですが、メモの検索機能を実装しています。

メモのタイトルやメモに含まれる各メッセージを対象とした文字列検索、アーカイブ(編集不能)状態のメモも表示するかやメモに設定しているタグを元にしたフィルタリングができるのと、中央ペインのメモ一覧を作成日や更新日順にソートできます。

あとメモはそれぞれ固有のIDを持っており、それによって検索もできます。

外部のチャットツール等と連携する場合、あるアセットについてチャットツールでほとんどやり取りしている場合もそのURLだけそのアセットのメモに残しておいたり、逆に UNote 側に情報を集約している場合はそのメモIDをチャットツール側に共有すればそれぞれ相互に素早く情報にアクセスできる想定です。

将来的には UnitySearch にも対応するかもしれませんが、基本的には現状の簡易的な検索だけ対応していれば十分な認識のため迷っています。UnitySearch のインデックスに対応できれば間接的にメモの検索高速化に期待できるため、多量にメモを取るようなプロジェクト用には意味があるかもしれませんが…。

Inspectorからメモを操作

アセットに対してのメモはメモエディターからだけではなくそのアセット選択時の Inspector からも操作できるようにしています。これはツールの制作を始めた時から追加を決めていた機能で、その方がメモへのアクセスが容易になって便利というのもあったのですがツール導入初期を想像してのことでした。

どのようなツールであれ、導入したての段階ではツールを使う習慣は全員に根付きません。そしてこのツールの場合は一部の導入した人がメモを残しても、それがメモエディターからしか確認できない場合メモを残された側はそれに気づけず無視する形になってしまい、残す側もそれでは意味がないため使用されなくなる可能性があります。そこで普通に Unity を使っていても自然と目に入るように Inspector にメモエディターを追加することにしました。

そんな方針で作成したのがこちらのクラスになります。
https://github.com/gok11/UNote/blob/main/Assets/UNote/Editor/Scripts/GUI/Inspector/NoteInjector.cs

動作の様子は次のような感じです。

Inspector への要素追加方法は3つほど思いつきました。

  1. Editor.finishedDefaultHeaderGUI から IMGUI の API で描画する
  2. Mono.Cecil で UnityEditor.dll を書き換えて必要な要素を追加する
  3. EditorApplication.update で必要な要素を追加する

1 について、IMGUI は書きなれているものの Unity の方針は UI Toolkit 推しで、実行パフォーマンスもやはり相対的に劣るので今回は考えないこととしました。

2 はやってみたくはあったのですが、保守コストが高くなると思われ同様の手法を採るエディター拡張があっても共存できない可能性もあるため不採用としました。

最終的に 3 で進めましたが、Inspector に表示される対象を正しく追う必要がある点に注意が必要でした。Selection の情報を見るだけだと Inspector が Lock 指定されていて Inspector で選択している対象と Selection の状態が一致しない場合に対応できなかったり Inspector 上部の Properties... から開ける PropertyEditor にも対応できないため、毎フレーム EditorWindow 全体の様子を見つつ ActiveEditorTracker を元に処理しています。

今は ProjectBrowser から開けるアセットでのみメモをできますが、近々 GameObject を対象としたメモができるよう拡張する予定です。

終わりに

UNote は今後もメモに関する機能を追加していきます。是非試してみてください。
また、ご意見などあればお気軽に Issue を立てるか @backlight1144 までお知らせください。
https://github.com/gok11/UNote

Discussion