【Unity】【Rider】Unity向けのIncremental Source GeneratorをイチからつくってUnityで動かすまでの手順まとめ

RiderでUnity向けのIncremental Source Generatorをイチからつくって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… を選択します。

Properties...

下図のウィンドウが開いたら、Target framework を netstandard2.0 に変更します。
最終的にDLLにするためC#のバージョンはUnityを意識せず適当なものを選んでください。

Target frameworkを選択

次に依存パッケージをインストールするため、再びプロジェクトを右クリックし、Manage NuGet Packages を選択します。

Manage NuGet Packages

Microsoft.CodeAnalysis.CSharp を検索し、適切なバージョンを選択します。
この「適切なバージョン」はマニュアルに記載されているのですが、実際には Unity のバージョンに応じてまちまちという状況になっております。この辺りの事情はneueccさんのブログをご参照ください。
本記事では下図の通り 4.3.0-3.final を使います(Incremental Source Generatorを使うには4系を使う必要があります)。

Microsoft.CodeAnalysis.CSharp

最後に、プロジェクトの設定を変更するため、プロジェクトをエディタ部分にドラッグ&ドロップします。

D&D

開いたら、以下のように <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 を作成します。

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… から、動作確認用の新しいプロジェクトを作成します。

Add New Project

Project TypeはConsole Applicationとして作成します。(動作確認したいだけなのでClass Libraryでも別にいいですが)

作成
プロジェクトが作成できたら、そのプロジェクトを右クリック > Properties を選択します。

次にプロジェクトをエディタ部分にドラッグ&ドロップし、以下のように 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プロジェクトの方は特にビルドする必要がないので、ビルド対象からはずしてもいいかもしれません)

Build whole solution

GeneratedProperty の参照を辿ると自動生成されたコードを確認できます。

自動生成されたコード

Unityにインポート・設定する

さてそれでは次にこれをUnityにインポートします。

まず上部のビルドボタンの右にあるドロップダウンからConfigurationをReleaseに変更して、ビルドボタンを押下します。

ビルドボタンを押下

Source Generatorのプロジェクトの直下の bin/Release/netstandard2.0/ExampleSourceGenerator.dll にある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);
        }
    }
}

参考

docs.unity3d.com

docs.unity3d.com