RiderでUnity向けのIncremental Source GeneratorをイチからつくってUnityで動かすまでの手順をまとめました。
- Incremental Source Generatorとは?
- ソリューションとプロジェクトを作成する
- 簡単なIncremental Source Generatorを作る
- Riderで動作確認をする
- Unityにインポート・設定する
- Unityで動作確認をする
- 参考
Rider 2024.3.6
Unity 2022.3.28f1
Incremental Source Generatorとは?
Incremental Source Generator を使うとコンパイル時に追加でソースコードを生成できます。
この時、既存のソースコードを参照できるため、例えば「既存のクラスの部分クラスを作ってプロパティを加える」など、ボイラープレートを自動生成するために使われたりします。
ちなみに Incremental Source Generator は .NET で提供されるコンパイラとコード解析ツールである Roslyn の機能の一つです。
また、これが作られるよりも前から存在した Source Generator(Incrementalではない)のパフォーマンス強化版という位置付けです。
本記事ではこの Incremental Source Generator をイチから作り、Unityで動かすまでの手順をまとめます。
なお、対象読者の大半であろう Unity エンジニアの多くはDLL開発に慣れていないと思われるので、Riderの操作手順については少々冗長にはなりますが丁寧にまとめます。
ソリューションとプロジェクトを作成する
まず Rider で新規の.NETソリューションを作成します。
Project Type は Class Library とします。
次にプロジェクトを右クリックして Properties… を選択します。
下図のウィンドウが開いたら、Target framework を netstandard2.0 に変更します。
最終的にDLLにするためC#のバージョンはUnityを意識せず適当なものを選んでください。
次に依存パッケージをインストールするため、再びプロジェクトを右クリックし、Manage NuGet Packages を選択します。
Microsoft.CodeAnalysis.CSharp を検索し、適切なバージョンを選択します。
この「適切なバージョン」はマニュアルに記載されているのですが、実際には Unity のバージョンに応じてまちまちという状況になっております。この辺りの事情はneueccさんのブログをご参照ください。
本記事では下図の通り 4.3.0-3.final を使います(Incremental Source Generatorを使うには4系を使う必要があります)。
最後に、プロジェクトの設定を変更するため、プロジェクトをエディタ部分にドラッグ&ドロップします。
開いたら、以下のように <IsRoslynComponent>true</IsRoslynComponent>
の行を追加します。
また、PropertyGroup に ImplicitUsings や Nullable といったプロパティがあったら不要なのでその行は削除します。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <LangVersion>11</LangVersion> <IsRoslynComponent>true</IsRoslynComponent> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0-3.final" /> </ItemGroup> </Project>
以上でプロジェクトのセットアップは完了です。
簡単なIncremental Source Generatorを作る
次に簡単なIncremental Source Generatorのコードを書きます。
プロジェクトを右クリック > Add > Class/Interface から ExampleGeneraor.cs を作成します。
中身は以下のように記述します。
using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace ExampleSourceGenerator; [Generator] public class ExampleGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { // 対象のクラス宣言を収集する var classDeclarations = context.SyntaxProvider .CreateSyntaxProvider( // 対象のシンタックスノードをフィルタリングする static (s, _) => { // クラス宣言だけを対象とする if (s is not ClassDeclarationSyntax classDeclaration) return false; // Namespaceが定義されているクラスだけを対象とする if (classDeclaration.Parent is not NamespaceDeclarationSyntax && classDeclaration.Parent is not FileScopedNamespaceDeclarationSyntax) return false; // ExampleあるいはExampleAttributeがついているクラスのみを対象とする return classDeclaration.AttributeLists .SelectMany(x => x.Attributes) .Any(x => { var name = x.Name.ToString(); return name == "Example" || name == "ExampleAttribute"; }); }, // フィルタリングされたノードから必要な情報を抽出する static (ctx, _) => (ClassDeclarationSyntax)ctx.Node); // 収集したクラス宣言に対してコードを生成する context.RegisterSourceOutput(classDeclarations, static (spc, classDeclaration) => { GenerateSource(spc, classDeclaration!); }); } private static void GenerateSource(SourceProductionContext context, ClassDeclarationSyntax classDeclaration) { var className = classDeclaration.Identifier.Text; var namespaceName = ((NamespaceDeclarationSyntax)classDeclaration.Parent!).Name.ToString(); // 「Hello, World!」という文字列をプロパティとして持つ部分クラスを生成する var source = $$""" using System; namespace {{namespaceName}} { public partial class {{className}} { public string GeneratedProperty { get; set; } = "Hello, World!"; } } """; var fileName = $"{className}.g.cs"; context.AddSource(fileName, SourceText.From(source, Encoding.UTF8)); } }
説明はコメントに記載した通りで、「Example」あるいは「ExampleAttribute」というアトリビュートが付いたクラスに対して、固定の文字列が代入されたプロパティを持つ部分クラスを生成しています。
Riderで動作確認をする
次に前節で作った Source Generator の動作確認を行います。
ソリューションを右クリック > Add > New Project… から、動作確認用の新しいプロジェクトを作成します。
Project TypeはConsole Applicationとして作成します。(動作確認したいだけなのでClass Libraryでも別にいいですが)
次にプロジェクトをエディタ部分にドラッグ&ドロップし、以下のように ItemGroup に Source Generator のプロジェクトのパスを追記します。
また、PropertyGroup に ImplicitUsings や Nullable といったプロパティがあったら不要なのでその行は削除します。
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0</TargetFramework> <LangVersion>11</LangVersion> <OutputType>Exe</OutputType> </PropertyGroup> <ItemGroup> <ProjectReference Include="..\ExampleSourceGenerator\ExampleSourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/> </ItemGroup> </Project>
続いてテスト用のクラスを作成します。
using System; namespace ExampleSourceGeneratorDev { public class ExampleAttribute : Attribute { } [Example] public partial class FooClass { } public static class Program { public static void Main(string[] args) { var foo = new FooClass(); Console.WriteLine(foo.GeneratedProperty); } } }
この時点では GeneratedProperty の部分でコンパイルエラーが出ているかもしれません。
上部のビルドボタンからビルドを行い、このコンパイルエラーが消えればコードが生成されています。
(ExampleSourceGeneratorDevプロジェクトの方は特にビルドする必要がないので、ビルド対象からはずしてもいいかもしれません)
GeneratedProperty の参照を辿ると自動生成されたコードを確認できます。
Unityにインポート・設定する
さてそれでは次にこれをUnityにインポートします。
まず上部のビルドボタンの右にあるドロップダウンからConfigurationをReleaseに変更して、ビルドボタンを押下します。
Source Generatorのプロジェクトの直下の bin/Release/netstandard2.0/ExampleSourceGenerator.dll
にあるDLLがビルドされていることを確認します。
これをUnityにドラッグ&ドロップしてインポートします。
asmdef を作ってあるフォルダ配下に入れるとそのアセンブリおよびそのアセンブリを参照するアセンブリが対象となります。
インポートしたら、そのアセットのInspectorから、プラットフォームのチェックを全て外し、さらに Asset Label に RoslynAnalyzer を設定します。
Unityで動作確認をする
あとは普通にUnityで以下のようなスクリプトを作って、コンパイルエラーが出なければOKです。
using System; using UnityEditor; using UnityEngine; namespace Example { public class ExampleAttribute : Attribute { } [Example] public partial class FooClass { } public static class Example { [MenuItem("Example/Test")] public static void Test() { var foo = new FooClass(); Debug.Log(foo.GeneratedProperty); } } }