見出し画像

Now in REALITY Tech #83 UnityEditor ComponentのInspectorを改善してみた話

🦊こん!にちは〜!
通りすがり今年2月にREALITYに参戦したばかりのド新人(?)&外国籍のUnityエンジニア、「デフォルトアバターじゃない者*」ですこ〜ん🌽!
この記事はREALITYの一員としての初記事です!

*次回の記事に変更(?)予定w

Slack上のアイコン: 顔があるので、デフォルトアバターじゃないということ!

REALITYとは

スマホ1つで格好いい可愛いアバターをぱぱーっと作成して顔バレなく配信できるアプリです!
是非使ってください!!
筆者はその魅力で入社しました。

格好いい・可愛い マイアバター

↓↓↓ GET IT NOW ↓↓↓

前書き 〜Inspector改善の経緯〜

私は入社してからいくつかの作業をやりまして、ちょっとやりづらいなーと感じた所がいっぱい出てきました。😂
例えば以下のInspectorですね。

🙂🤨🧐🙃😵‍💫😫🫠😵😇

一見だと問題ないと思ったが、このコンポーネントに着手すると大変なことになりました…
こういうことが数えきれないほど何ヶ所もあります!😂

「落ち着いてゆっくり読めばなんとかなる!」と思っている方がいらっしゃると思いますが、さっき言った通り、不具合修正や処理変更などの作業を行うと少し大変だと感じていませんか?
私は結構大変なので嫌ですねw。
このままだとこのコンポーネントだけではなく、全体的のメンテ効率が悪いでしょう。
特に長い間そのスクリプトを触っていない場合、久しぶりに触ると…なんと!
・このフィールドのオブジェクトはどれだー?!
 ┗ 一個一個クリックしてPingObjectで辿るのがしんどい 😫
・このオブジェクトは何??🤔
・あれ??? 私が実装したけど忘れたぜ! 分からんwww 😇
など…
はい! 地獄が見えてきましたね。
ということで、改善します!!

Inspector改善の結果

まずは関連しているSerializeFieldをグルーピングし、Unity Built-inのHeader属性を使って分かりやすくしました。

グルーピング & Header属性だけ使った

おおおおおおおおぉぉぉぉぉぉぉ!!
綺麗になりました!! 読みやすくなりました!!!
はい、終わり!😤




ではなく!!
これ以上の改善を求めている私はこのまま放ってはいかないんだ!🤣
も・ち・の〜ロン!🀄️
SerializeFieldに付けられるUnityのBuilt-in属性が少ないため、いっぱい改善したい場合は自分で拡張機能を実装しなければなりませんね。
幸い、Unity社はある程度自分で自由に拡張機能を書けるクラスを提供しています。
PropertyAttributeだと、その名は… PropertyDrawer !! と DecoratorDrawer !!
ということでいろいろ改善してみました。
とりあえず例のInspectorの進化の最終形態はこちら!
デデン!!

細かく分けました〜

区切りを実装し、見やすく分かりやすく修正しました〜 🎉

「えー? これだけ?」と思っている方々がいらっしゃると思いますが、あくまでも上記の例はこれだけで完結できたことです。
実際はいろんな属性を実装しました!
これらを使うと、CustomEditorを実装せず、簡単にInspectorを少しカスタマイズできます。

拡張属性の紹介

実装した属性の中に一部を紹介します。

Label属性
通常はフィールド名がラベルですが、この属性を付けると変更することができます。

[Label("Label属性を使う")]
[SerializeField] private int someSerializeField;
UnityのDefault Display Name→指定したラベルに変更

HelpBox属性
注意すべきことをInspectorに表示する。
例えば設定条件など。

[HelpBox("サンプルHelpBox エラー", HelpBoxMessageType.Error)]
[HelpBox("hogehoge")] // ↓画像のadsadsd...は長いので、ここはhogehogeに変更させていただきますw
[SerializeField] ...(略)

EnumLabel属性
上記のLabel属性と連動します。
本来UnityはEnumの名で表示しますが、一部の値を分かりやすくするため変更します。

private enum Hello
{
    [Label("おはよう")]
    Morning,
    [Label("こんにちは")]
    Afternoon,
    [Label("こんばんは")]
    Evening,
    Midnight,
}

[EnumLabel]
[SerializeField] private Hello hello;
上は何もしていない状態のInspector
下は通常のEnumのPopup選択肢
MidnightだけLabel属性付けていない

EnumSearchPopup属性
Enumの値を検索できる機能です。
上記のLabel属性と連動します。

UnityのデフォルトEnumPopupだと値を探すのが大変ですね。
想像してください。100個以上の要素だとどれくらい大変だろう。。。
そこで!この属性で解決できます。

private enum Hello
{
    [Label("おはよう")]
    Morning,
    [Label("こんにちは")]
    Afternoon,
    [Label("こんばんは")]
    Evening,
    Midnight,

    // 以降、検索テスト用のためいっぱい入れる
    a,s,d,f,g,h,j,k,l,q,w,e,r,t,y,u,i,o,p,z,x,c,v,b,n,m,
    aa,ss,dd,ff,gg,hh,jj,kk,ll,qq,ww,ee,rr,tt,yy,uu,ii,oo,pp,zz,xx,cc,vv,bb,nn,mm,
}

[EnumSearchPopup]
[SerializeField] private Hello hello;
開く時(通常のDropdown UIをクリック)
フィルター中 (1)
フィルター中 (2)
完全一致を1番上に表示

スクリプト実装の例

1属性につき、属性(Attribute)クラスとInspector描画(Drawer)クラスの作成が必要です。

筆者はこの機能を別アセンブリに実装しています。
Access Modifier (private/protected/public/internal)をうまく管理し、使用側に余計なアクセスできないようにしています。

一番簡単なLabel属性を見ましょう。

属性クラス
このクラスはSerializeFieldに付けるクラスです。

using System;
using System.Diagnostics;
using UnityEngine;

// フィールド以外使えないように AttributeTargets.Field
// 複数あるとバグりそうなので、 AllowMultiple = false
// 継承クラスも影響するため、 Inherited = true
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]

// Editorのみ使えるため、条件を付ける
[Conditional("UNITY_EDITOR")]

// UnityのSerializeフィールドのGUI描画を改造するため、PropertyAttributeクラスから継承
public sealed class LabelAttribute : PropertyAttribute
{
#if UNITY_EDITOR // 実機は不要なので囲む
    internal string Label { get; private set; }
#endif

    public LabelAttribute(string label)
    {
#if UNITY_EDITOR
        Label = label;
#endif
    }
}

Inspectorに描画するためのクラス
このクラスはEditorです。
internalにしてEditorアセンブリに配置しておくと、Runtimeアセンブリ編集中にIDEに打つ時はじゃまなクラス名が出てこないです。

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

// SerializeFieldの描画をカスタムするための属性
// 属性: CustomPropertyDrawer
// 引数: カスタムしたい(PropertyAttributeの派生クラス)属性の型
[CustomPropertyDrawer(typeof(LabelAttribute))]

// カスタムするため、PropertyDrawerから継承
internal sealed class LabelAttributeDrawer : PropertyDrawer
{
    // IMGUI用
    // 単純に受け取ったデフォルトラベルを属性で指定したラベルに置き換え
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var Attribute = (LabelAttribute)attribute;

        // 配列の要素じゃない場合のみ書き換え
        // 配列の要素の場合は表示を「Element 0, 1, 2, ...」のままにする
        // ※配列/リストのpropertyPathの末尾は "Array.data[i]" です。 (iは数字)
        if (!property.propertyPath.EndsWith(']'))
        {
            label.text = Attribute.Label;
        }
        EditorGUI.PropertyField(position, property, label, property.isExpanded);
    }

    // UI Element用
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var Attribute = (LabelAttribute)attribute;
        //-- (省略) --//
        return new PropertyField(property, Attribute.Label);
    }
}

上記は例として読みやすくしました。
実際はベースクラスを実装したり、Utilityクラスを実装したりしております。
こんな感じでゴリゴリ実装しまくってInspectorを改善することができました。✨✨✨

懸念点


PropertyAttribute(PropertyDrawerとDecoratorDrawer使用)には制限があります。
SerializeField*に付けないと使えないことです。
特にDecoratorDrawerはSerializeFieldの上になりますね。
自由にカスタムしたい場合は結局CustomEditorを実装しなければならないですが、20~30個のSerializeFieldがあるとしんどいですね。
早めにこの問題を解決してくれるUnityに祈っております。

*[SerializeField]属性の private / protected / internal もしくは public
※Serializable型


一部の描画関数はUnityEditorアセンブリにあるが公開(public)していないことです。
解決として、Reflectionを使いました。
ただ、将来のUnityアップデートでレイヤー変更などが行われると壊れますね。
グローバルフラグ用意し、デフォルト描画にFallbackするなどの対策が必要でしょう。
早めにpublicしてほしいですね。Unity様ぁぁ〜


最近のUnityはUI Element (Visual Element / UI Toolkit)を使ってInspectorを描画しています。
PropertyDrawerにOnGUI (IMGUI)だけ実装して、CustomEditorにVisualElementを使うと描画できなくなってしまいます。
そのため、両方の存在を考慮しながら実装しておかないとダメですね。
幸いVisualElementはIMGUIを簡単に描画できる方法があります。

//-- DecoratorDrawerの場合 --//
--------------------------------------------------
using UnityEngine.UIElements;

// ※クラスとメソッドの中身は省略
public override float GetHeight() {}
public override void OnGUI(Rect position) {}

// 描画用のVisualElementを作成
public override VisualElement CreatePropertyGUI()
{
    // Rootノードを作成
    var root = new VisualElement();
    // このノードの高さを設定
    root.style.height = GetHeight();
    // IMGUIを描画するノードを作成
    var container = new IMGUIContainer(() => OnGUI(root.contentRect));
    // Rootノードに追加
    root.Add(container);
    // Rootノードを返す
    return root;
}

ですが、OnGUIでしか使えないUnityEditorの変数をアクセスするとRuntimeエラーが発生しますね。orz
精一杯工夫して頑張ったんですが、できないものは仕方ありません!😭
UnityのOnGUI & UI Toolkit改善をお待ちしております!🥺

最後に

Editor拡張を書くことで、作業にはいろいろやりやすくなりますね。
Built-in機能ではないことで実装する手間がかかるが、メイン作業の効率アップするためにやっておいても損はないでしょう。
Editor拡張の書き方はググってみればたくさん出てきますので、それらの記事を参考すれば役に立てると思います。
また、新たなアイディアが浮かぶかもしれませんね。

追伸 FYI
UI Toolkitの正式表記は「UI Toolkit」ですね。「UI ToolKit」ではありません。
参照: https://docs.unity3d.com/Manual/UIElements.html