🫖

【Unity】Source Generator の使いみち

2023/12/10に公開

これはUnity Advent Calendar 2023の10日目の記事です。

https://qiita.com/advent-calendar/2023/unity


Unity 2021.2からC# Source Generatorが使えるようになってしばらく経ちました。

https://docs.unity3d.com/ja/current/Manual/roslyn-analyzers.html

いくつかのプロジェクトでSource Generatorをちゃんと導入してみて、どういう場面でSource Generatorが有用なのかということがわかってきたので、実例を交えて紹介してみたいと思います。

紹介するのは、大きく分けて3つの場面です。

  • 型のついてないものに型をつける
  • 高速で使いやすいメタプログラミング
  • ボイラープレートの排除

① 型のついてないものに型をつける

Unityでは基本的に静的だけども、文字列を使ってアクセスしないといけないものがよくあります。

  • エディタ拡張で指定するSerializedPropertyの名前
  • シェーダープロパティ名
  • アニメーションパラメータ名
  • タグ・レイヤー名

文字列ベースのAPIに共通の問題として、データ側の定義が変化した場合に、コンパイル時にエラーとして検出することができません。また、入力補完できないなどIDEビリティにも難があります。

こういったデータをSource Generatorを使って静的に定義すれば、これらの問題を解決できそうです。

たとえば、SerializedPropertyを通した値の操作を以下のように書けるようにできます。

using UnityEngine;
using UniTyped;
using UniTyped.Generated;

[UniTyped]
public class Example : MonoBehaviour
{
    [SerializeField] private int someValue = 0;
}

#if UNITY_EDITOR

[UnityEditor.CustomEditor(typeof(Example))]
public class ExampleEditor : UnityEditor.Editor
{
    public override void OnInspectorGUI()
    {
        // 自動生成されるExampleViewを通してアクセスする
        var view = new ExampleView()
        {
            Target = serializedObject
        };
        
        // serializedObject.FindProperty("someValue").intValue++; と同等
        view.someValue++;

        serializedObject.ApplyModifiedProperties();
        
    }
}

#endif

上記のようなSource Generatorをライブラリとしてまとめて公開しています。
https://github.com/ruccho/UniTyped

② 安全・高速なメタプログラミング手段

リフレクションを使いながらパフォーマンスを維持するためのテクニックはいろいろありますが、やはりオーバーヘッドを完全に排除することはできず、静的なコードがあるならそれが最強です。また、IL2CPPではExpressionTreeSystem.Reflection.Emit, dynamicなどの動的コード生成を行う機能は使用できません。

静的に記述可能なコードなら、Source Generatorを使って動的コード生成を避けることができます。Source GeneratorはC#コードを文字列として出力するのでILレベルのメタプログラミングと比較すると格段に安全です。また、生成結果はファイルとして出力されないためバージョン管理しやすいという利点もあります。

短所があるとすれば、既存のC#コードを書き換えられない点や、IL命令は出力できない点などがあります。現状ではIL Weavingをせざるを得ないユースケースもありえそうです。

Unityでの使い方として、以前こんなものをつくりました。

こちらのイベントエディタでは、UnityのAPIをノードとして呼び出すことができるようになっています。これらのノードはそれぞれがUnityのAPIからSource Generatorによって自動的に定義されており、リフレクションやDelegate.CreateDelegate()を使わずに実行されます。

Unityで使えるライブラリ類でも、Source Generator対応によりパフォーマンスや利便性を向上しているものがあります。よく利用されるものだとMemoryPackMessagePack for C#VContainerなどがありますね。

③ ボイラープレートの排除

なんだかんだこれが一番手近でやりやすい活用法かもしれません。

実際最近私がやった例としては、特定のインターフェースを実装したMonoBehaviourに対して共通のOnDrawGizmos()を実装したいことがありました。
対象のクラスをpartial指定すれば、Source Generator側でOnDrawGizmos()を追加することができます。

// ユーザー側
public partial class Hoge : MonoBehaviour, ISomething
{
}

// 生成コード
partial class Hoge
{
    private void OnDrawGizmos()
    {
        // 何らかの実装
    }
}

プロジェクトの事情に合わせて、開発の効率化につなげることができます。

こういう使い方の難点としては、Source Generatorの実装が別プロジェクトに分離してしまうという点があリます。汎用的なライブラリならまだしも、Unityプロジェクトと密結合した実装が別の.NETプロジェクトにあるというのはいささかイケてません。IDE上でワークスペースが分離してしまうのも面倒です。

おわり

Source Generatorが活用できる場面を見つけて、うまく使っていきましょう!

Discussion