えんじにあ雑記!

開発していて学んだことをまとめていきます!

Editor拡張コードを書くのが楽になる「UniTyped」使ってみた

Unity上でSerializedObjectやSerializedPropertyに対する型付けされたアクセスを提供するSource Generator「UniTyped」がリリースされていたので触ってみました。

UniTypedとは

UniTypedは、SerializedObject や SerializedPropertyに対し、型付けされたアクセスを提供するソースジェネレータです。より簡潔で安全なエディタ拡張コードを書くのに役立ちます。

とある通り、FindPropertyでSerializedPropertyとして取得し様々な操作を記述していた既存のコードから、Source Generatorにより生成された型付けされたより安全なコーディングができることを目的としたライブラリです。 また、生成されるコードは構造体ベースになっており、boxingを避けるような設計になっているため、ヒープのメモリ確保量が少なくパフォーマンスにも優れたものとなっています。

導入方法

Package Managerからhttps://github.com/ruccho/UniTyped.git?path=/Packages/com.ruccho.unitypedを入力してセットアップは完了します。

サンプルコード

[UniTyped]属性をMonoBehaviourやScriptableObject、そのほかSerializableなクラスに付与すると、UniTyped.Generated.[YourNamespace].[YourClass]Viewという構造体が使用できるようになります。

この生成された[YourClass]Viewという構造体のTargetプロパティにSerializedObjectインスタンスを渡したインスタンス経由でFindPropertyなどを必要とせずシリアライズされたプロパティの編集が可能となります。(※1)

using UnityEngine;
using UniTyped;

namespace Sample
{
    [UniTyped]
    public class Example01 : MonoBehaviour
    {
        [SerializeField]
        private int number;
    }

}
using Sample;
using UnityEditor;
using UnityEngine;
using UniTyped.Generated.Sample;

namespace Editor
{
    [CustomEditor(typeof(Example01))]
    public class Example01Editor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            Example01View view = new Example01View
            {
                Target = serializedObject
            };
            if (GUILayout.Button("+"))
            {
                view.number++;
            }

            if (GUILayout.Button("-"))
            {
                view.number--;
            }

            serializedObject.ApplyModifiedProperties();
        }
    }
}

UniTypedの動作確認

※1. サポートされている型に記載があるものはUniTypedのコード生成がサポートしています。

生成コードの調性方法

一部のフィールドを対象外にする

ignoreオプションをtrueに設定することで指定したフィールドをコード生成の対象外にすることができます。

using UnityEngine;
using UniTyped;

namespace Sample
{
    [UniTyped]
    public class Example01 : MonoBehaviour
    {
        [SerializeField]
        private int number;

        [SerializeField, UniTypedField(ignore = true)]
        private int number02;
    }

}
using Sample;
using UnityEditor;
using UnityEngine;
using UniTyped.Generated.Sample;

namespace Editor
{
    [CustomEditor(typeof(Example01))]
    public class Example01Editor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            Example01View view = new Example01View
            {
                Target = serializedObject
            };
            if (GUILayout.Button("+"))
            {
                view.number++;
            }

            if (GUILayout.Button("-"))
            {
                view.number--;
            }

            // ignore = true のためこれは生成されていない
            // view.number01++;
            
            serializedObject.ApplyModifiedProperties();
        }
    }
}

SerializedPropertyへのアクセスも提供する

UniTypedはデフォルトでは値の直接操作を可能にするために、アクセスプロパティを実際の値の型として公開します(フラット化)が、オプションを設定することでSerializedPropertyを公開することもできます。

using UnityEngine;
using UniTyped;

namespace Sample
{
    [UniTyped]
    public class Example01 : MonoBehaviour
    {
        [SerializeField]
        private int number;

        [SerializeField, UniTypedField(ignore = true)]
        private int number02;

        [SerializeField, UniTypedField(forceNested = true)]
        private int number03;
    }

}
using Sample;
using UnityEditor;
using UnityEngine;
using UniTyped.Generated.Sample;

namespace Editor
{
    [CustomEditor(typeof(Example01))]
    public class Example01Editor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            Example01View view = new Example01View
            {
                Target = serializedObject
            };
            if (GUILayout.Button("+"))
            {
                view.number++;
            }

            if (GUILayout.Button("-"))
            {
                view.number--;
            }

            // ignore = true のためこれは生成されていない
            // view.number01++;

            // forceNested = true のためSerializedPropertyへのアクセスも公開される
            Debug.Log($"SerializedProperty: {view.number03.Property}");
            Debug.Log($"Value: {view.number03.Value}");
            
            serializedObject.ApplyModifiedProperties();
        }
    }
}

Source Generatorが使用できない(Unity2021.2以前の環境)

UniTypedはRoslyn Source Generatorの機能を利用しているため、Unity2021.2以前の環境ではそのままでは利用できません。そういった環境向けにManual Generatorが提供されています。

Assemblyを切り分けている場合

とある機能を提供するModule01が存在し、RuntimeEditorでAssemblyを切り分けている場合、Runtime側に存在するコードに対して生成されるViewに対しても問題なくEditor側から使用できます。 Uni Typed Manual Generator Profileを使用して対象とする.csprojを選択しコード生成を手動で行うことが可能となっています。

Module01.Runtimeに下記のコードを配置。

using UnityEngine;
using UniTyped;

namespace Module01.Runtime
{
    [UniTyped]
    public class Module01Example : MonoBehaviour
    {
        [SerializeField]
        private int number;
    }
}

Module01.Editor側からViewを使用することができる。

using Module01.Runtime;
using UnityEditor;
using UnityEngine;
using UniTyped.Generated.Module01.Runtime;

namespace Module01.Editor
{
    [CustomEditor(typeof(Module01Example))]
    public class Module01ExampleEditor : UnityEditor.Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            Module01ExampleView view = new Module01ExampleView
            {
                Target = serializedObject
            };
            if (GUILayout.Button("Random"))
            {
                view.number = Random.Range(0, 100);
            }

            serializedObject.ApplyModifiedProperties();
        }
    }
}

まとめ

Source Generatorの機能を利用した新しいライブラリを使ってみた記事でした。UnityにおけるC#スクリプティングの環境は年々改良されており、Source Generatorに関しても利用したライブラリがいろいろと出てきてますが、意識せずコードが生成されているという開発者体験はかなり便利で個人的に好みです。

github.com