【Unity】Source Generator の使いみち
これはUnity Advent Calendar 2023の10日目の記事です。
Unity 2021.2からC# Source Generatorが使えるようになってしばらく経ちました。
いくつかのプロジェクトで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をライブラリとしてまとめて公開しています。
② 安全・高速なメタプログラミング手段
リフレクションを使いながらパフォーマンスを維持するためのテクニックはいろいろありますが、やはりオーバーヘッドを完全に排除することはできず、静的なコードがあるならそれが最強です。また、IL2CPPではExpressionTree
やSystem.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対応によりパフォーマンスや利便性を向上しているものがあります。よく利用されるものだとMemoryPackやMessagePack 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